整体流程

vue2.x 源码

  1. Data 通过 observer 转换成了 getter/setter 的形式来追踪变化
  2. 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中
  3. 当数据发生了变化时,会触发 setter,从而向 Dep 中的依赖 (即 Watcher) 发送通知
  4. Watcher 接收到通知后,向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等

Observer

监测数据,给数据打上响应式标记: __ob__
在 getter 中收集依赖,在 setter 中通知依赖更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAument;
augment(value, arrayMethods, arrayKeys);
// 深度侦测
this.observeArray(value);
} else {
this.walk(value);
}
}

walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}

observeArray(items) {
for (let i = 0, l = items.lengtgh; i < l; i++) {
observe(items[i]);
}
}
}

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach((method) => {
const origianl = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = origianl.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "shift":
inserted = false; // 如果是push或shift方法,传参即为新增的元素
break;
case "splice":
inserted = args.slice(2); // 如果是splice方法,传入参数列表中下标为2的即为新增的元素
break;
}
if (inserted) ob.observeArray(inserted); // 调用observe函数将新增元素转化为响应式
ob.dep.notify();
return result;
});
});

export const hasProto = "__proto__" in {};

const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

function protoAugment(target, src) {
target.__proto__ = src;
}

function copyAument(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}

export function def(obj, key, val, enumerable = true) {
Object.defineProperty(obj, key, {
value: val,
enumerable,
writable: true,
configurable: true,
});
}

export function observe(value) {
if (!isObject(value) || value instanceof VNode) return;
let ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}

export function defineReactive(obj, key, val) {
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === "object") {
new Observer(val);
}

const dep = new Dep();
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (childOb) {
childOb.dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) return;
val = newVal;
dep.notify();
},
});
}

Dep

依赖收集器

  • 渲染页面时碰到插值表达式,v-bind 等需要数据的地方,会实例化一个 watcher
  • 实例化 watcher 就会对依赖的数据求值,从而触发 getter,数据的 getter 函数就会添加依赖自己的 watcher,从而完成依赖收集
  • 可以理解为 watcher 在收集依赖,而代码的实现方式是在数据中存储依赖自己的 watcher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export class Dep {
constructor() {
this.subs = [];
}

addSub(sub) {
this.subs.push(sub);
}

removeSub(sub) {
remove(this.subs, sub);
}

depend() {
// window.target即为 watcher
if (window.target) {
this.addSub(window.target);
}
}

notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}

export function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}

Watcher

逻辑生成依赖,并添加到 Dep 中

Watcher 先把自己设置到全局唯一的指定位置 (window.target),然后读取数据。触发这个数据的 getter。接着,在getter 中就会从全局唯一的那个位置读取当前正在读取数据的 Watcher,并把这个 watcher 收集到 Dep 中去。收集好之后,当数据发生变化时,会向 Dep 中的每个 Watcher 发送通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
export class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();

if (options) {
// ...
this.computed = !!options.computed; // 是否为计算属性
// ...
} else {
// ...
}

this.dirty = this.computed; // 标志计算属性的返回值是否有变化,用于缓存,true代表重新计算
if (typeof expOrFn === "function") {
this.getter = expOrFn;
}

if (this.computed) {
this.value = undefined;
this.dep = new Dep();
}
}

// 在模板编译函数中的实例化watcher的,getter中取不到这个实例。解决方法也很简单,将watcher实例放到全局,比如放到window.target上。
get() {
// 不能写window.target = new Watcher()。因为执行到getter的时候,实例化watcher还没有完成,所以window.target还是undefined
window.target = this;
const vm = this.vm;
// 获取一下被依赖的数据, 触发该数据上面的getter, 从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去
let value = this.getter.call(vm, vm);
// 处理 watch 监听中的 deep 属性
if (this.deep) {
traverse(value);
}
window.target = undefined;
return value;
}

update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);

if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true;
} else {
// 调用getAndInvoke去对比计算属性的返回值是否发生了变化,如果真的发生变化,则执行回调,通知那些读取计算属性的watcher重新执行渲染逻辑
this.getAndInvoke(() => {
this.dep.notify();
});
}
}
}

// 计算属性取缓存还是重新计算逻辑
evaluate() {
if (this.dirty) {
this.value = this.get();
this.dirty = false;
}
return this.value;
}

depend() {
if (this.dep && Dep.target) {
this.dep.depend();
}
}

getAndInvoke(cb) {
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`);
}
} else {
cb.call(this.vm, value, oldValue);
}
}
}

// 用于取消 watch
teardown() {
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
}
}

问题理解

只要触发 getter 就会收集依赖吗

不会

  • 在 Dep 的 depend 方法中,只有 Dep.target 为真时才会添加依赖。比如在派发更新时会触发 watcher 的 update 方法,该方法也会触发 parsePath 来取值,但是此时的 Dep.target 为 null,不会添加依赖
  • 仔细观察可以发现,只有 watcher 的 get 方法中会调用 pushTarget(this) 来对 Dep.target 赋值,其他时候 Dep.target 都是 null,而 get 方法只会在实例化 watcher 的时候调用
  • 因此,在实现中,一个 watcher 的依赖在其实例化时就已经确定了,之后任何读取值的操作均不会增加依赖

依赖嵌套的对象属性

1
2
3
let w2 = new Watcher(obj, "b.m.n", (val, oldVal) =>
console.log(`obj.b.m.n 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
);

w2 会依赖 obj.b.m.n,但是 w2 会依赖 obj.b, obj.b.m 吗?或者说,obj.b 和 obj.b.m,它们闭包中保存的 dep 中会有 w2 吗

  • 如果让 obj.b = null,那么很显然 w2 的回调函数应该被触发,这就说明 w2 会依赖中间层级的对象属性
  • 代码层面: new Watcher() 时,会调用 watcher 的 get 方法,将 Dep.target 设置为 w2,get 方法会调用 parsePath 来取值
  1. 局部变量 obj 为对象 obj,读取 obj.b 的值,触发 getter,触发 dep.depend() (该 dep 是 obj.b 的闭包中的 dep),Dep.target 存在,添加依赖
  2. 局部变量 obj 为 obj.b,读取 obj.b.m 的值,触发 getter,触发 dep.depend() (该 dep 是 obj.b.m 的闭包中的 dep),Dep.target 存在,添加依赖
  3. 局部变量 obj 为对象 obj.b.m,读取 obj.b.m.n 的值,触发 getter,触发 dep.depend() (该 dep 是 obj.b.m.n 的闭包中的 dep),Dep.target 存在,添加依赖