《Vue.js设计与实现》——分支切换与 cleanup

《Vue.js设计与实现》------响应式系统的基本实现

在上一篇文章中我们实现了一个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,所以oktext两个字段都会被读取,即两个字段都会和该副作用函数建立联系,然后我们按下面的步骤更改值:

  1. ok的值改为false
  2. text的值改为'change text'
javascript 复制代码
setTimeout(() => {
    proxyObj.ok = false
}, 2000)

setTimeout(() => {
    proxyObj.text = 'change text'
}, 3000)

我们期望的结果是:

  1. ok的值变为false,触发执行副作用函数,页面上的值变为not,控制台打印'effect func'
  2. text的值更改,但因为ok的值是false,text的值已经并不会被访问到,所以就算text值变动了,页面应无变动,控制台应无新增输出,即这个副作用函数没有执行的必要

但实际上更改text的值后还是会触发执行副作用函数,控制台会多一次输出

解决分支切换问题

分析

在上面的的例子中,出现非期望的情况的原因是:text字段和副作用函数还存在着联系,即在修改值后触发trigger执行时查找出来的副作用函数集合中还有该副作用函数。

所以解决办法就是每次在执行副作用函数之前,切断这个副作用函数和所有字段的联系 ,然后在执行过程中再重新建立字段和副作用函数之间的联系,这样字段和副作用函数之间的联系就是全新且正确的了。

切断这个副作用函数和所有字段的联系 用代码的说法就是:在所有的Set集合中,把这个副作用函数删除,那么我们就需要:

  1. 收集这个副作用函数关联了哪些Set
  2. 在执行函数前遍历这些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() })

我们来梳理一下问题出现的执行过程:

  1. 修改ok的值,触发set,进入forEach循环,遍历执行副作用函数fn。
  2. 执行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() }) // 删除
}

最后的运行结果:

小结

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

相关推荐
m0_471199635 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥6 分钟前
Java web
java·开发语言·前端
A小码哥8 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays8 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi11 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat12 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524712 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏13 分钟前
CSS盒模型(Box Model) 原理
前端·css
web前端12314 分钟前
React Hooks 介绍与实践要点
前端·react.js
我是小疯子6614 分钟前
JavaScriptWebAPI核心操作全解析
前端