# 依赖收集
依赖收集收集的是 Watcher。Wacher 是观察者,用于订阅数据的变化,当响应式数据变化的时候,会触发对应的 setter。
# Dep
Vue.js关于依赖收集相关的类是Dep,位置在core/observer/dep.js,主要收集的方法为dep.depend,调用的地方在defineReactive中的getter,这就是依赖收集的地方。
export default class Dep {
// 静态属性,watcher对象
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
if (process.env.NODE_ENV !== "production" && !config.async) {
subs.sort((a, b) => a.id - b.id);
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep是一个类,他有一个静态属性target,Dep.target用来存储当前的Watcher,subs用来存储各种Watcher,包括render watcher,computed watcher,user watcher。depend就是收集依赖notify,当数据变化的时候,会通知dep.notify(),这个时候就会执行这个方法,最终执行watcher.update(),这个属于派发更新。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建依赖对象,一个对象的属性对应一个依赖
const dep = new Dep();
// 是否需要递归观察对象的值
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// 收集依赖
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
// 返回get属性对应的值
return value;
},
});
}
在new Observer和defineReactive的时候会进行new Dep,我们通过代码来看下一共有几个 dep 实例:
export default {
data() {
return {
obj: {
msg: "hello",
},
};
},
};
一开始会初始化整个 data,然后 observe(data),会执行 new Observer 实例化,这个时候有了第一个 dep
// {id:0,subs:[]}
const dep = new Dep();
之后会对整个对象进行 walk,循环进行 defineReactive({obj:{msg:'hello'}},'obj'),这时候会有第二次的 dep
// {id:1,subs:[]}
const dep = new Dep();
在 defineReactive中,又会对 childObj 进行 observe,由于{msg:'hello'}也是对象,所以又会重新 new Observer
// {id:2,subs:[]}
const dep = new Dep();
之后会对整个对象进行 walk,循环进行 defineReactive({msg:'hello'},'msg'),这时候会有第四次的 dep
// {id:3,subs:[]}
const dep = new Dep();
所以总共会进行 4 次的 dep 实例化分别是一开始根对象dep,两次defineReactive函数中的dep,childObj中的dep,这里要注意的是,默认有两个 dep 实例'$attrs','$listeners'。
# Dep.target
当初次渲染的时候,会调用new Watcher(),最终执行this.get(),在get中,会pushTarget(this),这里的this就是当前的Watcher对象
Watcher下面的get函数:
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
而pushTarget定义在Dep类内,很明显的是Dep.target就是存储的Watcher对象
Dep.target = null;
const targetStack = [];
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
# Watcher
Watcher也是一个类,定义在core/observer/watcher.js
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
invokeWithErrorHandling,
noop,
} from "../util/index";
import { traverse } from "./traverse";
import { queueWatcher } from "./scheduler";
import Dep, { pushTarget, popTarget } from "./dep";
import type { SimpleSet } from "../util/index";
let uid = 0;
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
}
// computed watcher lazy = true
this.value = this.lazy ? undefined : this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
/**
* Add a dependency to this directive.
*/
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);
}
}
}
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;
}
update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
run() {
if (this.active) {
const value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value;
this.value = value;
if (this.user) {
const info = `callback for watcher "${this.expression}"`;
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
evaluate() {
this.value = this.get();
this.dirty = false;
}
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
teardown() {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}
可以很明显的看到new Watcher最后会执行this.get(),之后会将Dep.target设置为当前的Watcher,执行getter函数,这个函数就是我们渲染时候定义的函数。最后调用popTarget(), 会把targetStack最后一个移除,同时设置Dep.target为倒数第二个。
这里用了一个栈的数据结构,是为了处理父子组件的关系, 父子组件嵌套的时候把父组件的 watcher 先入栈,再去处理子组件的 watcher,子组件处理完毕后,在把父组件 watcher 出栈,子组件渲染完毕后,才会父组件才算渲染完毕,也就是child mounted -> parent mounted,这样可以保证组件在渲染的过程中保持正确的依赖。
# dep.depend
在defineReactive中的getter会进行依赖收集dep.depend()
// observer/index.js
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// 收集依赖
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
// 返回get属性对应的值
return value;
},
});
// Dep.js
class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
}
在Dep定义可以看到,有Dep.target就是Watcher实例,也就是会调用addDep方法,同时传入Dep实例
class Watcher {
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);
}
}
}
}
这里首先会获取 dep 实例的 id,然后判断是否在新的依赖中存在 id,如果没有,将 depId 存入 newDepIds,将 dep 存入 newDeps,在判断当前的 id 是否在旧的 depIds,如果没有,则调用 dep.addSub 方法,将当前的 watcher传入,也就是当前的dep实例下的subs有这个watcher实例。
这里主要是为处理同一个依赖的情况:
<template>
<div>
{{ age }}
{{ age }}
</div>
</template>
当模版页面渲染的时候,render 函数遇到 age 变量,就会触发 getter 进行依赖收集,将 age 所处的 dep 实例存入 newDepIds,第二次遇到 age 的时候,又回触发 getter,但是由于已经在 newDepIds 存在,就直接跳过。
在收集依赖的逻辑中,还有一个逻辑:
if (childOb) {
// 2. 如果属性对应的值是对象或者数组,也需要收集依赖来处理数组或者对象的变化 this.name.push(1)
childOb.dep.depend();
// 3. 如果属性的值是数组,又会重新判断数组内的每一个值是否为数组或对象,如果是,又会重新收集依赖 this.name = [1,[2,3],4]
if (Array.isArray(value)) {
dependArray(value);
}
}
收集依赖一共有三种情况需要考虑:
export default {
data() {
return {
name: [1, 2, {}, [1, 2]],
};
},
};
- 属性: 每一个属性都是一个
dep,所以需要dep.depend(),比如this.name = '1',发生变化。 - 属性值: 如果属性值是对象或者数组,也需要收集依赖,比如
this.name.push(1)。 - 属性值如果是数组: 还需要判断值是否为响应式对象,如果是也需要收集依赖
this.name = [1,2,{a:1,__ob__:__ob__}]。最后如果数组的值还是数组,需要递归处理。
# cleanupDeps
当我们执行完渲染函数的最后,会进行cleanupDeps对依赖进行清除
class Watcher {
// 精简代码
constructor() {
this.deps = []; // 旧dep
this.newDeps = []; // 新dep
this.depIds = new Set(); // 旧dep id
this.newDepIds = new Set(); // 新dep id
}
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;
}
}
假设我们的代码:
<div id="app">
<p v-if="a===1">{{b}}</p>
<button @click="change" type="button">click me</button>
</div>
<script>
new Vue({
el: "#app",
data: {
a: "a",
b: "b",
},
methods: {
change() {
this.a = 1;
},
},
});
</script>
首次渲染的时候,只会触发 a 的 getter,所以:
this.deps = [];
this.newDeps = [{ id: 3, subs: [Watcher] }];
渲染完成之后执行 cleanupDeps,会将 newDep 的 id 和内容都放入 deps,然后清空自身:
this.deps = [{ id: 3, subs: [Watcher] }];
this.newDeps = [];
当我们执行按钮的时候,会触发两个 getter,分别是 a 和 b,其中 a 的值为 1,b 的值为'b',首先是 a,由于 a 的 dep 之前已经存入了 deps,所以不会执行 dep.addSub,所以这个时候:
this.deps = [{ id: 3, subs: [Watcher] }];
this.newDeps = [
{ id: 3, subs: [Watcher] },
{ id: 4, subs: [Watcher] }, // 4 就是b的getter初始化的dep实例
];
最后同样会执行 cleanupDeps:
this.deps = [
{ id: 3, subs: [Watcher] },
{ id: 4, subs: [Watcher] },
];
this.newDeps = [];
这样的好处是可以避免无关的依赖重复渲染组件,执行回掉函数等。