分支切换与cleanup
分支切换
在effect函数中使用三元表达式:
const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})
其中effectFn中的三元表达式,会根据obj.ok的值的变化而切换到不同的代码分支。即当obj.ok的值发生变化时,会执行不同代码分支,这就是所谓的分支切换。
分支切换存在的问题
分支切换可能会存在遗留的副作用函数:
以上边代码为例,若obj.ok初始值为true,会读取obj.text的值,即此时会触发ok和text这两个字段的读操作,那么此时effectFn会与这两个字段产生联系:
data
└── ok
└── effectFn
└── text
└── effectFn
此时,副作用函数被ok和text这两个字段段依赖收集。但是当我把ok的值设为false,理想情况下:因为text不会被读取,那么此时是副作用只依赖于ok这一个字段。但是实际上,副作用函数仍然依赖于ok和text两个值,那么此时遗留的副作用函数会触发不必要的更新。
解决思路
解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数,那么问题就迎刃而解。
如何解决?
要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,那么我们可以在effect内部定义新的effectFn,并为effectFn添加deps属性,这是一个数组,用于存储所有的包含当前函数的依赖集合。
// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn) {
const effectFn = () => {
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
那么这个effectFn.deps中的元素是怎么收集的呢?其实是在track函数中进行收集
function track(target, key) {
// 没有 activeEffect,直接 return
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 中
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps) // 新增
}
在为代理对象收集当前活跃的副作用函数的同时,副作用函数也将当前依赖这个函数的属性的依赖集合收集起来,那么此时在副作用函数端也可以看到收集了自己的依赖集合,即新增的代码是建立了这个关系:
建立这个联系之后,每次副作用函数执行的时候,从effectFn.deps获取所有收集了自己的依赖集合,并将自身从这些集合中清除。
那么我们在cleanup函数执行上述的清除操作:
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
至此,副作用函数遗留的问题解决。但是在运行代码的时候,会发现进入了无限循环,问题出现在trigger中:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn()) // 问题出在这句代码
}
在trigger中,我们遍历effect集合,而集合中存储着副作用函数。而**我们在执行副作用函数时,会调用cleanup清除依赖,但是副作用函数执行的时候又触发了读操作,又把副作用函数收集进依赖集合中。**此时等效于下面代码:
const set = new Set([1])
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中')
})
这段代码中,这个循环会一直执行下去。
set有关的语言规范
其实上述循环涉及到:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问,因此上述循环会无限执行。
解决方法
对这种情况,我们拷贝一个set进行遍历即可:
const set = new Set([1])
const newSet = new Set(set)
newSet.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中')
})
那么在trigger函数中,我们可以进行如下修改:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects) // 新增
effectsToRun.forEach(effectFn => effectFn()) // 新增
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
相同的,我们拷贝effects为effectsToRun进行遍历,即可避免无限执行。