当我们改变某个响应式数据触发 setter 时,会执行该数据的 Dep 中的所有 Watcher,也就是会执行 new watcher 时保存的 回调函数如(computed 的 getter)。执行这个回调函数的时候会重新读取依赖的响应式数据,从而触发 getter 进行依赖收集。
- 那么在这个过程中如何保证 Dep 不会重复收集 Watcher 呢?
watcher去重
Vue 采用的方式是给每个 Dep 对象引入 id ,Watcher 对象中记录所有的 Dep 的 id,下次重新收集依赖的时候,如果 Dep 的 id 已经存在,就不再收集该 Watcher 了。
let uid = 0;
export default class Dep {
constructor() {
this.id = uid++;
this.subs = []; // 保存所有需要执行的函数
}
// ......
}
export default class Watcher {
constructor(Fn) {
this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
}
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
}
- 如何移除不需要的依赖呢?
依赖清理
Vue 采用的方法是:重新收集依赖时,用一个变量newDeps来记录新一次的依赖,之后再和旧的 Dep 对象列表比对,如果发现多余依赖,就将该依赖的 Watcher 从 Dep 中移除。
export default class Dep {
// ......
removeSub(sub) {
remove(this.subs, sub);
}
// ......
}
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set();
this.deps = [];
this.newDeps = []; // 记录新一次的依赖
this.newDepIds = new Set();
this.get();
}
get() {
Dep.target = this;
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
/* 重点关注 */
this.cleanupDeps();
}
return value;
}
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
}
在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。
对象添加属性
使用 Object.defineProperty 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的,比如:
var vm = new Vue({
data:{
a:1
}
})
// vm.b 是非响应的
vm.b = 2
Vue 为了解决这个问题,定义了一个全局 API Vue.set 方法。
数组元素添加或删除
对于 vue 无法检测到数组元素的添加或删除的问题,通过将数组的7种原生方法重写为可以截获响应的方法,再将数组的每个成员进行 observe 来解决。
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)
/*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
/*将数组的原生方法缓存起来,后面要调用*/
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
/*调用原生的数组方法*/
const result = original.apply(this, args)
/*数组新插入的元素需要重新进行observe才能响应式*/
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
/*dep通知所有注册的观察者进行响应式处理*/
ob.dep.notify()
return result
})
})