在上一篇文章中我们实现了一个Vue的最基本的响应式系统,这节我们来尝试解决其中存在的一个问题------分支切换。
分支切换例子
javascript
// 响应式系统的基本实现代码参考上一节《Vue.js设计与实现》------响应式系统的基本实现
let obj = { text: 'hello responsive data', ok: true }
watchEffectFn(() => {
console.log('effect func');
document.body.innerText = proxyObj.ok ? proxyObj.text : 'not'
})
该例子初始状态下,因为ok的值是true,所以ok 和text两个字段都会被读取,即两个字段都会和该副作用函数建立联系,然后我们按下面的步骤更改值:
- ok的值改为false
- text的值改为'change text'
javascript
setTimeout(() => {
proxyObj.ok = false
}, 2000)
setTimeout(() => {
proxyObj.text = 'change text'
}, 3000)
我们期望的结果是:
- ok的值变为false,触发执行副作用函数,页面上的值变为not,控制台打印'effect func'
- text的值更改,但因为ok的值是false,text的值已经并不会被访问到,所以就算text值变动了,页面应无变动,控制台应无新增输出,即这个副作用函数没有执行的必要
但实际上更改text的值后还是会触发执行副作用函数,控制台会多一次输出

解决分支切换问题
分析
在上面的的例子中,出现非期望的情况的原因是:text字段和副作用函数还存在着联系,即在修改值后触发trigger执行时查找出来的副作用函数集合中还有该副作用函数。
所以解决办法就是每次在执行副作用函数之前,切断这个副作用函数和所有字段的联系 ,然后在执行过程中再重新建立字段和副作用函数之间的联系,这样字段和副作用函数之间的联系就是全新且正确的了。
切断这个副作用函数和所有字段的联系 用代码的说法就是:在所有的Set集合中,把这个副作用函数删除,那么我们就需要:
- 收集这个副作用函数关联了哪些Set
- 在执行函数前遍历这些Set,删除当中的对应副作用函数
下面我们尝试实现这两步。
实现
收集这个副作用函数关联了哪些Set集合
diff
// 重写watchEffectFn函数
function watchEffectFn(fn) {
// 实际的副作用函数被包裹了一层
const effectFn = () => {
activeEffectFn = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
function track(target, key, receiver) {
// ...前面代码省略
+ // 这边记录副作用函数在哪些Set中出现过
+ activeEffectFn.deps.push(deps)
}
清除副作用函数和Set集合之间的联系
diff
function watchEffectFn(fn) {
const effectFn = () => {
+ // 执行副作用函数前清除和Set集合之间的联系
+ cleanup(effectFn)
activeEffectFn = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
cleanup函数的实现如下:
javascript
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
let deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
缺陷修复(无限执行)
大功告成,运行代码,看看效果。好好好,修改ok的值时第一次触发set直接进入无限执行打印(🤯
问题出在cleanup和trigger中的以下循环执行副作用函数的逻辑冲突:
javascript
effects && effects.forEach(fn => { fn() })
我们来梳理一下问题出现的执行过程:
- 修改ok的值,触发set,进入forEach循环,遍历执行副作用函数fn。
- 执行fn的过程中会执行cleanup函数清除联系,然后执行具体函数过程时又重新建立联系添加到集合中,相当于边删除边添加,由此就导致了无限执行。
语言规范中对此有明确的说明:在调用 forEach 遍历 Set 集合 时,如果一个值已经被访问过了,但该值被删除并重新添加到集合, 如果此时 forEach 遍历没有结束,那么该值会重新被访问。---------《Vue设计与实现》
具体修改如下, 具体执行时创建集合的副本来执行:
diff
function trigger(target, key, newVal, receiver) {
const depsMap = targetBucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
+ const effectsToRun = new Set(effects) // 新增
+ effectsToRun.forEach(fn => fn()) // 新增
- effects && effects.forEach(fn => { fn() }) // 删除
}
最后的运行结果:

小结
由此我们就解决了响应式系统中的分支切换问题,有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~