vue3是通过Proxy来拦截对数据的操作,实现响应式的。
- 为什么执行副作用函数前要有清除操作(cleanup)?
- 为什么加了清除上一次的依赖关系操作后 effects.forEach(fn => fn())会变成死循环
- 为什么要支持嵌套effect?
- 在注册副作用函数中既读取属性值,又设置属性值时,会发生堆栈溢出?
let activeEffect = null
// 副作用函数存储栈,保留外层副作用函数,解决问题3
let 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] = newValue
trigger(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 = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
function track(target, key){
if(!activeEffect) return
let 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) return
let 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());
}