具体代码实现
// 存储副作用函数 const bucket = new WeakMap() // 用于存储被注册的副作用函数 let activeEffect = null // 用于接收并注册副作用函数 function effect(fn) { const effectFn = () => { // 先调用 cleanup 函数完成旧依赖的清除工作 cleanup(effectFn) // 保存 fn activeEffect = effectFn // 执行 fn 函数,目的是初始化执行和触发 get 拦截 fn() } // 用于存储所有与其关联的副作用函数的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } // 清除本次依赖相关的旧副作用函数 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 数组 effectFn.deps.length = 0 } // 响应式数据 function reactive(target) { return new Proxy(target, { get(target, key) { // 没有注册副作用函数,直接返回数据 if (!activeEffect) return Reflect.get(target, key) track(target, key) return Reflect.get(target, key) }, set(target, key, newVal) { target[key] = newVal trigger(target, key) return Reflect.set(target, key, newVal) } }) } // 收集依赖 function track(target, key) { // 从 bucket 获取 depsMap 的依赖关系 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 从 depsMap 获取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } // 触发依赖 function trigger(target, key) { // 获取对应的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 获取对应的 deps const effects = depsMap.get(key) // 构建新的 Set 避免递归 const effectsToRun = new Set(effects) // 执行相应的 effect effectsToRun.forEach(effectFn => effectFn()) } 复制代码
支持嵌套的 effect 函数
为什么要支持嵌套 effect 函数?
用 Vuejs 来举例,如组件的嵌套就需要支持嵌套的 effect 函数,伪代码如下:
存在缺陷
假设存在如下的嵌套关系,存在的缺陷如下:
- 当定时器执行并只更改
data.text
的值,此时只有 effect2 执行了,而期望的 effect1 却没执行执行 - 原因是 目前使用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,意味着同一时刻 activeEffect 存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层的副作用函数的执行会覆盖 activeEffect 的值,当外部响应式数据进行依赖收集时,它们收集到的副作用函数将会是内层的副作用函数
// 获得响应式数据 const data = reactive({ text: 'hello world...', ok: true }) // 注册副作用函数 effect(() => { effect(() => { console.log('effect2 执行:', data.ok) }) console.log('effect1 执行:', data.text) }) console.log("bucket = ", bucket); setTimeout(() => { console.log('setTimeout 执行,修改 data.text 的值') data.text = 'hello vue3...' }, 1000); 复制代码
完善思路
通过副作用函数栈 effectStack 将正在执行的副作用函数入栈,等到副作用函数执行完毕后再弹出栈,并保证 activeEffect 始终是指向栈顶的副作用函数。
具体代码实现
// 存储副作用函数 const bucket = new WeakMap() // 用于存储被注册的副作用函数 let activeEffect = null // effect 栈 const effectStack = [] // 用于接收并注册副作用函数 function effect(fn) { const effectFn = () => { // 先调用 cleanup 函数完成旧依赖的清除工作 cleanup(effectFn) // 保存 fn activeEffect = effectFn // 在副作用函数调用前,将副作用函数入栈 effectStack.push(effectFn) // 执行 fn 函数,目的是初始化执行和触发 get 拦截 fn() // 副作用函数执行完成后出栈 effectStack.pop() // 将 activeEffect 指向栈顶(原先)的副作用函数 activeEffect = effectStack[effectStack.length - 1] } // 用于存储所有与其关联的副作用函数的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } // 清除本次依赖相关的旧副作用函数 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 数组 effectFn.deps.length = 0 } // 响应式数据 function reactive(target) { return new Proxy(target, { get(target, key) { // 没有注册副作用函数,直接返回数据 if (!activeEffect) return Reflect.get(target, key) track(target, key) return Reflect.get(target, key) }, set(target, key, newVal) { target[key] = newVal trigger(target, key) return Reflect.set(target, key, newVal) } }) } // 收集依赖 function track(target, key) { // 从 bucket 获取 depsMap 的依赖关系 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 从 depsMap 获取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } // 触发依赖 function trigger(target, key) { // 获取对应的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 获取对应的 deps const effects = depsMap.get(key) // 构建新的 Set 避免递归 const effectsToRun = new Set(effects) // 执行相应的 effect effectsToRun.forEach(effectFn => effectFn()) } 复制代码
避免无限递归循环
存在缺陷
如下面的例子,就会产生无限循环:
// 获得响应式数据 const data = reactive({ count: 1 }) // 注册副作用函数 effect(() => { data.count++ // 等价于 data.count = data.count + 1 }) 复制代码
其中,既会读取 data.count
的值,又会设置 data.count
的值,每次 trigger 操作触发时,本次还没有执行完,又触发了下一次的 trigger 操作,这就会产生无限递归调用自身,导致栈溢出。
完善思路
在 trigger 操作发生时添加是否执行副作用函数的条件:若 trigger 触发执行的副作用函数与当前的正则执行的副作用函数相同,则不触发执行。
具体代码实现
// 存储副作用函数 const bucket = new WeakMap() // 用于存储被注册的副作用函数 let activeEffect = null // effect 栈 const effectStack = [] // 用于接收并注册副作用函数 function effect(fn) { const effectFn = () => { // 先调用 cleanup 函数完成旧依赖的清除工作 cleanup(effectFn) // 保存 fn activeEffect = effectFn // 在副作用函数调用前,将副作用函数入栈 effectStack.push(effectFn) // 执行 fn 函数,目的是初始化执行和触发 get 拦截 fn() // 副作用函数执行完成后出栈 effectStack.pop() // 将 activeEffect 指向栈顶(原先)的副作用函数 activeEffect = effectStack[effectStack.length - 1] } // 用于存储所有与其关联的副作用函数的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } // 清除本次依赖相关的旧副作用函数 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 数组 effectFn.deps.length = 0 } // 响应式数据 function reactive(target) { return new Proxy(target, { get(target, key) { // 没有注册副作用函数,直接返回数据 if (!activeEffect) return Reflect.get(target, key) track(target, key) return Reflect.get(target, key) }, set(target, key, newVal) { target[key] = newVal trigger(target, key) return Reflect.set(target, key, newVal) } }) } // 收集依赖 function track(target, key) { // 从 bucket 获取 depsMap 的依赖关系 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 从 depsMap 获取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } // 触发依赖 function trigger(target, key) { // 获取对应的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 获取对应的 deps const effects = depsMap.get(key) // 构建新的 Set 避免递归 const effectsToRun = new Set(effects) // 执行相应的 effect effectsToRun && effectsToRun.forEach(effectFn => { // 避免递归调用自身 if (effectFn !== activeEffect) effectFn() }) }