effect嵌套
开发中我们肯定不会只收集render函数,在render函数中肯定还会想收集其他的副作用函数,如某些methods,我们也想在对应的依赖改变的时候重新执行对应的methods,这样我们在使用effect函数时就会发生嵌套,如下代码
// 原始数据
const data = { foo: true, bar: true }
let temp1, temp2
effect(function render() {
console.log('render 执行')
effect(function effectFn1() {
console.log('effectFn1 执行')
temp2 = dataProxy.bar
})
temp1 = dataProxy.foo
})
setTimeout(() => {
dataProxy.foo = false
}, 1000);
执行上面代码我们会发现,一秒后 effectFn1 执行 重新打印了一遍,这种结果显然是不对的,我们一秒后改变的是foo的值,那么应该会将render函数重新执行一遍,应该把 render 执行 和 effectFn1 执行 都再次打印一遍。
其实问题很简单,我们逐步分析一下。当执行外层的effect时,全局的activeEffect的值为render函数,在执行render函数的过程中又遇到内层的effect,所以此时代码会将activeEffect的值赋值为effectFn1函数,而当内层的effect执行完成后去执行temp1 = dataProxy.foo这一行代码我们会发现,此时全局的activeEffect值依然是effectFn1函数,所以foo就收集到effectFn1函数而不是render函数,所以后来执行foo相关依赖的时候就执行的是effectFn1函数。
相信到这里大家应该都能想到对应的解决方案,我们只需要保证全局的activeEffect的值是正确的就可以了。我们这里使用一个栈结构来存储所有层级effect对应的副作用函数,我们需要保证activeEffect永远指向栈顶的副作用函数,当栈顶的副作用函数执行完成后将其弹出即可,以下为具体实现
// effect 栈
const effectStack = []
function effect(fn) {
const effectFn = () => {
// 清除所有被收集的当前副作用函
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn)
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 存储所有收集effectFn的集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
同时赋值取值
前面我们编写的用来测试的render副作用函数只对代理对象进行了读取操作,当我们尝试同时进行赋值取值的操作时会发现新的问题,如下render函数
function render() {
dataProxy.flag = !dataProxy.flag
}
effect(render)
运行代码我们会发现报了一个栈溢出的错误:Maximum call stack size exceeded
原因很简单,我们执行dataProxy.flag = !dataProxy.flag时是先执行读的操作的,所以当读取到dataProxy.flag的值时,render函数已经被flag对应的集合收集到了,当我们对flag进行赋值操作的时候,render函数会被取出来再次执行,这样就进入了一个死循环。
解决方案很简单,我们在取依赖执行的时候,要判断一下取出来执行的依赖函数是不是当前全局的依赖函数,如果是就不要执行
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
}
以上就是两个问题的具体解释及处理,下节将会实现调度器和懒执行配置,这两个配置是实现computed和watch的关键