《Vue.js设计与实现》——副作用函数实现可嵌套

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

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

在前面的两篇文章中我们实现了一个最基本的响应式系统,并解决了系统中的分支切换情况,这一节我们继续完善系统,使得副作用函数之间可以嵌套。

为什么副作用函数需要可以嵌套

组件的渲染函数render其实也是在一个副作用函数中执行的,而我们在编写组件里会经常引入其他组件使用,组件允许嵌套,那么副作用函数也需要可以嵌套。

对于一个Foo组件来说,effect这样执行他的渲染函数:

javascript 复制代码
// Foo组件
const Foo = {
    render() {
    }
}
effect(() => {
    Foo.render()
})

如果Foo组件中引入使用了Bar组件,则会变成下面的样子:

diff 复制代码
effect(() => {
    Foo.render()
+   // 组件嵌套
+    effect(() => {
+        Bar.render()
+    })
})

副作用函数不可嵌套的弊端

javascript 复制代码
// 原始数据
const data = { foo: true, bar: true } 

// 代理对象
const obj = new Proxy(data, { /* ... */ })

let temp1 = null
let temp2 = null

effect(function effectFn1() {
    console.log('读取foo属性')
    effect(function effectFn2() {
        console.log('读取bar属性')
        temp2 = proxyObj.bar
    })
    temp1 = proxyObj.foo
})

在上面的例子中,foo字段关联的副作用函数effectFn1如下:

javascript 复制代码
function effectFn1() {
    console.log('读取foo属性')
    effect(function effectFn2() {
        console.log('读取bar属性')
        temp2 = proxyObj.bar
    })
    temp1 = proxyObj.foo
}

bar字段关联的副作用函数effectFn2如下:

javascript 复制代码
function effectFn2() {
    console.log('读取bar属性')
    temp2 = proxyObj.bar
}

foo关联的副作用函数嵌套了bar字段关联的副作用函数,在这种情况下分别修改两个字段,我们期望的结果是:

  1. 修改foo字段时会打印
    • 读取foo属性
    • 读取bar属性
  2. 修改bar字段时会打印
    • 读取bar属性

但我们尝试修改foo字段后会发现打印的结果却是下面这样的,不仅没有打印两次,反而执行了effectFn2内的打印:

实现副作用函数可嵌套

分析

在实现我们想要的效果之前,先来分析一下出现不是我们期望结果的原因。

javascript 复制代码
effect(function effectFn1() {
    console.log('读取foo属性')
    effectn(function effectFn2() {
        console.log('读取bar属性')
        temp2 = proxyObj.bar
    })
    temp1 = proxyObj.foo
})

let activeEffectFn = null
function effect(fn) {
    const effectFn = () => {
        cleanup(effectFn) // 新增
        activeEffectFn = effectFn
        fn()
    }
    effectFn.deps = []
    effectFn()
}

问题在于这两步的顺序:

javascript 复制代码
effect(function effectFn2() {
    console.log('读取bar属性')
    temp2 = proxyObj.bar
})
javascript 复制代码
temp1 = proxyObj.foo
  1. 执行到第一步时把activeEffectFn改为了effectFn2
  2. 函数执行完毕后执行第二步时activeEffectFn不会变回temp1 = proxyObj.foo应绑定的副作用函数effectFn1,foo字段也和effectFn2建立了联系,所以才导致我们修改foo的值时执行的是effectFn2

实现

如果你有兴趣的话把这两步的顺序换一下,就会发现和我们期望的结果是一致的了,当然我们还是需要在不改动执行顺序的情况下达成效果。

改动如下:

diff 复制代码
let activeEffectFn = null
// 副作用函数栈
+const effectStack = []
function effect(fn) {
    const effectFn = () => {
        cleanup(effectFn) // 新增
        activeEffectFn = effectFn
        // 在调用副作用函数之前将当前副作用函数压入栈中
+        effectStack.push(effectFn)
        fn()
        // 结束之后,弹出副作用函数
+        effectStack.pop()
        // 把activeEffectFn改为栈顶的函数
+        activeEffectFn = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}

我们增加一个变量effectStack用于存储副作用函数(其实就是数组 ),在每次执行副作用函数之前将其压入栈中(就是push 到最末尾),执行结束后再弹出(就是删除 最后一个值),并把activeEffectFn设置为栈顶的值(就是最后一个值),然后我们再来看看执行的结果。

这样就符合我们的期望了。

避免无限递归循环

问题出现的情况和分析

我们把副作用函数写成下面的样子:

javascript 复制代码
effect(() => {
    proxyObj.num++
})

就会发现控制台报错如下:

问题在于proxyObj.num++这一步,这一步我们其实可以看成proxyObj.num = proxyObj.num + 1,这一步中我们不仅读取(get) 了num,也设置(set) 了num,在get时执行的副作用函数执行过程中触发了set,又会去执行该副作用函数,如此就导致了无限递归。

我们对trigger函数做如下修改即可解决问题,如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行:

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)
+    const effectsToRun = new Set()
+    effects && effects.forEach((effect) => 
+        // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行   
+        if (effect !== activeEffectFn) {
+            effectsToRun.add(effect)
+        }
+    })
    effectsToRun.forEach(fn => fn())
}

(有和我一样的在执行啦?那我不执行了,我不打扰,我走了哈🏃‍♂️

小结

本文我们实现了副作用函数的可嵌套,下一节中我们尝试实现系统的可调度性,有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~

参考书籍

《Vue.js设计与实现》------霍春阳

相关推荐
white-persist4 分钟前
Python实例方法与Python类的构造方法全解析
开发语言·前端·python·原型模式
新中地GIS开发老师42 分钟前
Cesium 军事标绘入门:用 Cesium-Plot-JS 快速实现标绘功能
前端·javascript·arcgis·cesium·gis开发·地理信息科学
Superxpang1 小时前
前端性能优化
前端·javascript·vue.js·性能优化
左手吻左脸。1 小时前
解决el-select因为弹出层层级问题,不展示下拉选
javascript·vue.js·elementui
左手吻左脸。1 小时前
Element UI表格中根据数值动态设置字体颜色
vue.js·ui·elementui
李白的故乡1 小时前
el-tree-select名字
javascript·vue.js·ecmascript
Rysxt_1 小时前
Element Plus 入门教程:从零开始构建 Vue 3 界面
前端·javascript·vue.js
隐含1 小时前
对于el-table中自定义表头中添加el-popover会弹出两个的解决方案,分别针对固定列和非固定列来隐藏最后一个浮框。
前端·javascript·vue.js
大鱼前端1 小时前
Turbopack vs Webpack vs Vite:前端构建工具三分天下,谁将胜出?
前端·webpack·turbopack
你的人类朋友1 小时前
先用js快速开发,后续引入ts是否是一个好的实践?
前端·javascript·后端