什么是副作用函数?
如果一个函数只是接收输入,然后输出,没有影响外部(比如修改外部变量),那么我们就说这个函数没有副作用,反之就是有副作用。理想的情况下,我们希望所有的函数都很纯粹,没有副作用。
举个例子:
1 | function effect() { |
Proxy
我们都知道 vue.js 这类框架是声明式的,当数据发生变化,它可以监听到并重新渲染,那么这种响应系统是如何实现的呢?首先一个问题就是怎么监听到数据的变化。答案是 Proxy,vue2 用了Object.defineProperty()
函数,这是 es2015 之前的做法,vue3 则是用了 es2015 带来的新特性:Proxy
。
通过 Proxy 我们可以监听到数据的变化(set),那就可以调用副作用函数更新 dom 了。
最简单的响应系统设计如下:
1 | function effect(data) { |
下面开始,都是需求变化产生的代码变化了。
- 如果有多个副作用函数需要注册,怎么办?
- 如果这些多个副作用函数,监听的是这个对象的不同 key,怎么办?
- 如果这些多个副作用函数,监听的是多个对象的不同 key,怎么办?
- 分支切换
如果有多个副作用函数需要注册,怎么办?
代码如下:
1 | function effect1(data) { |
这样写,显然很不灵活(因为要去修改已经写好的代码),那么怎么设计才能不去动已经写好的代码呢?答案是传回调函数:
1 | const bucket = new Set(); |
这样的写法,是不是很像 react 的 useEffect()了。
如果这些多个副作用函数,监听的是这个对象的不同 key,怎么办?
用个 Map,让 key 和回调函数一一对应即可,但如何知道副作用函数用了哪个 key 呢?如果是在 useEffect 里面显然是不知道的,但用没用 key,用了哪个 key,Proxy 里面的 get 拦截函数是一清二楚的,所以我们应该在这里添加副作用函数!但 get 拦截函数里面又不知道,当前获取数据的函数是哪个,很简单,我们设置一个 activeEffect 来记录当前的函数。但需要注意的是,一但我们开始在 get 里面注册响应,每次调用副作用函数就会调用注册响应,这个时候需要更新 activeEffect。
1 | let activeEffect; |
这样就实现了对单个对象的多个不同 key 注册多个副作用函数,进行响应式渲染。
而且,我们可以把其中处理副作用函数的逻辑抽出来:
1 | let activeEffect; |
如果这些多个副作用函数,监听的是多个对象的不同 key,怎么办?
再加一个 Map 即可,而且由于键是个对象,最好用 WeakMap:
1 | let activeEffect; |
分支切换
目前来说:
- 首次执行副作用函数会触发 get,get 会把副作用函数添加为响应函数。
- 然后更新值的时候会触发 set,set 会执行响应函数,执行响应函数又会触发 get,get 会把副作用函数添加为响应函数。
也就是说每次更新值都会重新添加响应函数,似乎很冗余,能否只执行一次呢?答案是不能,因为可能存在分支,例如:
1 | const data = { ok: true, text: "hello world" }; |
当 obj.ok 为 true 的时候,obj.text 的响应函数会记录下副作用函数,但如果 obj.ok 为 false 的时候,修改 obj.text 其实不用执行副作用函数了,但由于之前添加过,所以还是会执行。
所以我们不仅要重新添加响应函数,还需要清理之前添加的。这样每次添加的响应函数才会是准确无误的。
为了清理响应函数,比较粗暴一点的是遍历 bucket 中的每个 target 的每个 key,然后对其 set 集合执行 delete()方法,但这样显然不太好,我们可以记录一下哪些集合存了当前副作用函数,只对这些集合执行 delete()方法。
代码如下:
1 | function useEffect(fn, data) { |
我们改写了 useEffect,增加了一个 records 来记录需要清理哪些集合,在每次重新添加响应之前,清理掉旧的响应。并在 track 函数里面对这个 records 进行填充。
但还存在一个问题,这个问题比较隐秘,那就是遍历的时候对遍历对象进行 add 和 delete 操作,trigger 中我们执行响应函数,响应函数会对桶先 cleanup 再重新添加,但这个时候我们还在遍历桶啊,所以就相当于这样:
1 | const set = new Set([1]); |
这会造成死循环,解决的办法,自然是不对遍历的对象增删,新搞个对象进行增删。修改后的代码如下:
1 | function trigger(target, key) { |