Vue的依赖收集和性能问题
什么是依赖收集
Vue能够实现当一个数据变更时,视图就进行刷新,而且用到这个数据的其他地方也会同步变更;而且,这个数据必须是在有被依赖的情况下,视图和其他用到数据的地方才会变更。 所以,Vue要能够知道一个数据是否被使用,实现这种机制的技术叫做依赖收集。
每个组件实例都有相应的watcher实例 - 渲染组件的过程,会把属性记录为依赖 - 当我们操纵一个数据时,依赖项的setter会被调用,从而通知watcher重新计算,从而致使与之相关联的组件得以更新。
Vue2用defineProperty来劫持属性,生成watcher实例来响应属性的变化。
注意,Dep的target是watcher实例
依赖收集与观察者模式
在Vue依赖收集里:谁是观察者?谁是观察目标? 显然: - 依赖的数据是观察目标(Watcher) - 视图、计算属性、侦听器这些是观察者Watcher
依赖收集分析
依赖收集的三个类:
Dep:发布者。扮演观察目标的角色,每一个数据都会有Dep类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观察者,当本数据变更时,调用dep.notify()通知观察者
Watcher:观察者。扮演观察者的角色,进行观察者函数的包装处理。如render()函数,会被进行包装成一个Watcher实例
Observer:观测类。辅助的可观测类,数组/对象通过它的转化,可成为可观测数据
每一个数据都有的Dep类实例:
由于JavaScript是单线程模型,所以虽然有多个观察者函数,但是一个时刻内,就只会有一个观察者函数在执行,那么此刻正在执行的那个观察者函数,所对应的Watcher实例,便会被赋给Dep.target这一类变量,从而只要访问Dep.target就能知道当前的观察者是谁。 在后续的依赖收集工作里,getter里会调用dep.depend(),而setter里则会调用dep.notify()
Watcher观察者
一个组件里可以有多个Watcher类实例,Watcher类包装观察者函数,而观察者函数使用数据。 观察者函数经过Watcher是这么被包装的:
- 模板渲染:this._watcher = new Watcher(this, render, this._update)
- 计算属性:
computed: {
name() {
return `${this.firstName} ${this.lastName}`;
}
}
/*
会形成
new Watcher(this, function name() {
return `${this.firstName} ${this.lastName}`
}, callback);
*/
在Watcher类里做的事情,概括起来则是:
1、传入组件实例、观察者函数、回调函数、选项:
先解释清楚4个变量:deps、depIds、newDeps、newDepIds,它们的作用如下:
- deps:缓存上一轮执行观察者函数用到的dep实例
- depIds:Hash表,用于快速查找
- newDeps:存储本轮执行观察者函数用到的dep实例
- newDepIds:Hash表,用于快速查找
2、进行初始求值,初始求值时,会调用watcher.get()方法
3、watcher.get()会做以下处理:初始准备工作、调用观察者函数计算、事后清理工作
①在初始准备工作里,会将当前Watcher实例赋给Dep.target,清空数组newDeps、newDepIds
②执行观察者函数,进行计算。由于数据观测阶段执行了defineReactive(),所以计算过程用到的数据会得以访问,从而触发数据的getter,从而执行watcher.addDep()方法,将特定的数据记为依赖
③对每个数据执行watcher.addDep(dep)后,数据对应的dep如果在newDeps里不存在,就会加入到newDeps里,这是因为一次计算过程数据有可能被多次使用,但是同样的依赖只能收集一次。并且如果在deps不存在,表示上一轮计算中,当前watcher未依赖过某个数据,那个数据相应的dep.subs里也不存在当前watcher,所以要将当前watcher加入到数据的dep.subs里
④进行事后清理工作,首先释放Dep.target,然后拿newDeps和deps进行对比,接着进行以下的处理:
- newDeps里不存在,deps里存在的数据,表示是过期的缓存数据。相应的,从数据对应的dep.subs移除掉当前watcher
- 将newDeps赋给deps,表示缓存本轮的计算结果,这样子下轮计算如果再依赖同一个数据,就不需要再收集了
⑤当某个数据更新时,由于进行了setter拦截,所以会对该数据的dep.subs这一观察者队列里的watchers进行通知,从而执行watcher.update()方法,而update()方法会重复求值过程(即为步骤3-7),从而使得观察者函数重新计算,而render()这种观察者函数重新计算的结果,就使得视图同步了最新的数据
Vue的依赖收集,是观察者模式的一种应用。其原理总结如图:
- 配置依赖观测
- 收集依赖
- 数据值变更
依赖收集带来的问题:
依赖收集需要对数据元进行递归绑定。当数据元数据量过大(例如明细表或透视表数十万条数据),会严重影响前端的性能效率。
解决方法:
①避开依赖收集:
不使用this.xxx = data绑定数据
对数据使用Object.freeze进行冻结(避免依赖收集,Vue2对configurable为false的对象不进行数据劫持)或者将数据绑定为class私有属性
②将数据对象约定为数组