这篇文章我们来处理一个问题:分支切换可能会造成不必要的依赖被收集。
这里的分支切换指的是三元运算符,而不是git中的概念,我们在代码中时常用到三元表达式,比如:
// 原始数据
const data = {
flag: true,
text: 'hello world'
}
// 对原始数据的代理
const dataProxy = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
trigger(target, key)
}
})
function render() {
console.log('num')
return dataProxy.flag ? dataProxy.text : 'other message'
}
effect(render)
上面代码我们在render中使用了三元运算符,可以看到flag和text属性都收集到了render的依赖函数,当我们改变这两个属性的值时,render函数将会被再次执行。
这看上去并没有什么问题,但是当我们将flag赋值为false的时候,render函数将永远不会执行到dataProxy.text,也就是说此时的render函数不应该被text属性收集,那么我们改如何处理这个问题呢?
其实很简单,在我们每次执行render之前,将上次收集到的所有render依赖移除掉,然后执行render的时候再收集相应的依赖,这样就能保证不会有多余的依赖,具体实现:
function effect(fn) {
// 将fn的执行使用effectFn嵌套一层,方便后续操作
const effectFn = () => {
// 清除所有被收集的当前副作用函
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
fn()
}
// 存储所有收集effectFn的集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
effectFn.deps[i].delete(effectFn)
}
// 清空deps数组
effectFn.deps.length = 0
}
function track(target, key) {
// 省略部分代码
// 将当前集合收集到副作用函数中的deps数组里 方便清除
activeEffect.deps.push(deps)
}
以上,我们定义一个effectFn函数对fn进行包裹,然后给effectFn一个deps属性用来收集所有收集过effectFn的依赖集合,以便在每次执行fn前的清除工作,而deps数组的填充也非常简单,我们只需要在track时将当前key对应的依赖集合push到deps中即可。
如果此时你尝试运行代码会发现会进入到死循环,原因很简单,这是因为集合遍历的一个特性,如果我们在遍历过程中对集合进行了增删操作,那么forEach就会再次执行,如下面代码
const set = new Set([1])
// 解决办法就是重新定义一个set 对新set进行遍历即可
// const newSet = new Set(set)
set.forEach(item => {
set.delete(1)
set.add(1)
console.log(999)
})
其实我们的trigger函数中也有类似操作,我们在对effects进行遍历的时候用于会执行cleanup然后重新收集依赖,所以就会出现无限循环的情况,而解决方法也很简单,只需要定义一个新的set
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 定义一个新的set 遍历新set即可
const effectsToRun = new Set()
effects && effects.forEach(effectFn => effectsToRun.add(effectFn))
effectsToRun.forEach(effectFn => effectFn())
}
这样我们就解决了因为分支切换造成的不必要的依赖被收集的问题