接上篇:Vue2剥丝抽茧-响应式系统 ,没看的同学需要先看一下。
场景
我们考虑一下下边的代码会输出什么。
1 | import { observe } from "./reactive"; |
我们来一步一步理清:
observer(data)
拦截了 data
中 text
和 ok
的 get、set
,并且各自初始化了一个 Dep
实例,用来保存依赖它们的 Watcher
对象。
new Watcher(updateComponent);
这一步会执行 updateComponent
函数,执行过程中用到的所有对象属性,会将 Watcher
收集到相应对象属性中的Dep
中。
当然这里的 Watcher
其实是同一个,所以用了指向的箭头。
data.ok = false;
这一步会触发 set
,从而执行 Dep
中所有的 Watcher
,此时就会执行一次 updateComponent
。
执行 updateComponent
就会重新读取 data
中的属性,触发 get
,然后继续收集 Watcher
。
重新执行 updateComponent
函数 的时候:
1 | const updateComponent = () => { |
因为 data.ok
的值变为 false
,所以就不会触发 data.text
的 get
,text
的 Dep
就不会变化了。
而 data.ok
会继续执行,触发 get
收集 Watcher
,但由于我们 Dep
中使用的是数组,此时收集到的两个 Wacher
其实是同一个,这里是有问题,会导致 updateComponent
重复执行,一会儿我们来解决下。
data.text = "hello, liang";
执行这句的时候,会触发 text
的 set
,所以会执行一次 updateComponent
。但从代码来看 updateComponent
函数中由于 data.ok
为 false
,data.text
对输出没有任何影响,这次执行其实是没有必要的。
之所以执行了,是因为第一次执行 updateComponent
读取了 data.text
从而收集了 Watcher
,第二次执行 updateComponent
的时候,data.text
虽然没有读到,但之前的 Watcher
也没有清除掉,所以这一次改变 data.text
的时候 updateComponent
依旧会执行。
所以我们需要的就是当重新执行 updateComponent
的时候,如果 Watcher
已经不依赖于某个 Dep
了,我们需要将当前 Watcher
从该 Dep
中移除掉。
问题
总结下来我们需要做两件事情。
- 去重,
Dep
中不要重复收集Watcher
。 - 重置,如果该属性对
Dep
中的Wacher
已经没有影响了(换句话就是,Watcher
中的updateComponent
已经不会读取到该属性了
),就将该Watcher
从该属性的Dep
中删除。
去重
去重的话有两种方案:
Dep
中的subs
数组换为Set
。- 每个
Dep
对象引入id
,Watcher
对象中记录所有的Dep
的id
,下次重新收集依赖的时候,如果Dep
的id
已经存在,就不再收集该Watcher
了。
Vue2
源码中采用的是方案 2
这里我们实现下:
Dep
类的话只需要引入 id
即可。
1 | /*************改动***************************/ |
在 Watcher
中,我们引入 this.depIds
来记录所有的 id
。
1 | import Dep from "./dep"; |
重置
同样是两个方案:
- 全量式移除,保存
Watcher
所影响的所有Dep
对象,当重新收集Watcher
的前,把当前Watcher
从记录中的所有Dep
对象中移除。 - 增量式移除,重新收集依赖时,用一个新的变量记录所有的
Dep
对象,之后再和旧的Dep
对象列表比对,如果新的中没有,旧的中有,就将当前Watcher
从该Dep
对象中移除。
Vue2
中采用的是方案 2
,这里也实现下。
首先是 Dep
类,我们需要提供一个 removeSub
方法。
1 | import { remove } from "./util"; |
然后是 Watcher
类,我们引入 this.deps
来保存所有的旧 Dep
对象,引入 this.newDeps
来保存所有的新 Dep
对象。
1 | import Dep from "./dep"; |
测试
回到开头的代码
1 | import { observe } from "./reactive"; |
此时 data.text
修改的话就不会再执行 updateComponent
了,因为第二次执行的时候,我们把 data.text
中 Dep
里的 Watcher
清除了。
总
今天这个主要就是对响应式系统的一点优化,避免不必要的重新执行。所做的事情就是重新调用函数的时候,把已经没有关联的 Watcher
去除。
不知道看到这里大家有没有一个疑问,我是一直没想到说服我的点,欢迎一起交流:
在解决去重问题上,我们是引入了 id
,但如果直接用 set
其实就可以。在 Watcher
类中是用 Set
来存 id
,用数组来存 Dep
对象,为什么不直接用 Set
来存 Dep
对象呢?