vue3是通过Proxy来拦截对数据的操作,实现响应式的。
- 为什么执行副作用函数前要有清除操作(cleanup)?
- 为什么加了清除上一次的依赖关系操作后 effects.forEach(fn => fn())会变成死循环
- 为什么要支持嵌套effect?
- 在注册副作用函数中既读取属性值,又设置属性值时,会发生堆栈溢出?
let activeEffect = null// 副作用函数存储栈,保留外层副作用函数,解决问题3let effectStack = []// 存储收集到的响应式数据// Map{target: Map{key: Set[...effects]}}const bucket = new WeakMap()const data = {text: 'Hello world!',ok: true}let dataProxy = new Proxy(data, {get(target, key){track(target, key)return target[key]},set(target, key, newValue){target[key] = newValuetrigger(target, key)}})effect(() => {document.body.innerHTML = dataProxy.ok ? dataProxy.text : 'not'})dataProxy.ok = false// 此方法是为了清除当前副作用函数之前执行时收集到的依赖,解决问题1// 例如此例中,当 dataProxy.ok 为 false 时,dataProxy.text将不会再被读取,// 也就是说之后修改text不应该再触发 dataProxy 的 set 去执行副作用函数function cleanup(effectFn){effectFn.deps.forEach(deps => deps.delete(effectFn))effectFn.deps.length = 0}// 注册副作用函数function effect(fn){const effectFn = () => {cleanup(effectFn)activeEffect = effectFneffectStack.push(effectFn)fn()effectStack.pop()activeEffect = effectStack[effectStack.length - 1]}effectFn.deps = []effectFn()}function track(target, key){if(!activeEffect) returnlet depsMap = bucket.get(target)if(!depsMap){bucket.set(target, depsMap = new Map())}let deps = depsMap.get(key)if(!deps){depsMap.set(key, deps = new Set())}deps.add(activeEffect)activeEffect.deps.push(deps)}function trigger(target, key){let depsMap = bucket.get(target)if(!depsMap) returnlet effects = depsMap.get(key)// effects && effects.forEach(fn => fn());// 由上面的书写方式改为下面的 解决问题2// 原因: 当对dataPorxy的属性(key)进行修改时,trigger执行 => 获取key对应的effects =>// effects执行(即effect函数中effectFn执行) => cleanup函数执行 => key对应的effects中的值被删除// effect函数传入的fn执行 => dataPorxy的属性get触发 => key对应的effects中的值又被添加// 从上述过程可以看出: 获取key对应的effects 中的值被删除又重新添加,导致死循环// 见规范: https://262.ecma-international.org/6.0/#sec-set.prototype.foreach// 规范内容:每个值通常只被访问一次。但是,如果一个值在被访问后被删除,然后在调用完成之前被重新添加,则该值将被重新访问。// 例子:// const set = new Set([1])// set.forEach(item => {// set.delete(1)// set.add(1)// console.log('执行中')// })let effectToRun = new Set()effects && effects.forEach(effect => {// 解决问题4,避免副作用函数递归调用,栈溢出// 原因:当副作用函数中读取值后副作用函数会执行,在执行过程中 又设置了该属性值,// 便触发trigger函数执行,在trigger中又执行副作用函数,如此递归导致爆栈// 解决:当当前执行的副作用函数是 effect时,就不再执行了if(effect !== activeEffect) effectToRun.add(effect)});effectToRun.forEach(fn => fn());}
