Vue Reactivity Principle and Implementation

✍🏼 Written on Aug 2, 2016   
❗️ Note: it has been days since this article was written, please be aware of its timeliness

Origin of This Article

After studying Vue’s reactivity principle, I wanted to document it and implement Vue’s reactivity system from scratch, hence this article.

Overview

Vue The initialization of reactive data occurs in initState’s initData, where the observe function observes the data properties of the vm object and then sets the getter and setter properties.
Thus, at least three functions are required: an observer for setting properties getter and setter, a trigger for getter and setter’s 监听者, and a 收集框 to store property dependencies. I named them observe, watcher, and dep, respectively.

Process

First, in observe, the properties in data are recursively augmented with getter and setter. Later, when accessing one of the properties in watcher, the watcher instance triggers the getter interceptor. The interceptor for that property then adds the watcher instance to the property’s closure-based dependency collector dep, while also placing this closure-based dependency collector into the watcher.

Subsequently, when modifying the aforementioned property, the setter interceptor defined in observe is triggered. At this point, the watcher in dep starts working, executing the callback of that watcher.

Notes

How to prevent duplicate dependency collection when triggering getter?

When watcher triggers getter, it adds the current property’s dependency collector to watcher’s Set array for comparison. If it’s a duplicate, it won’t be added again:

1
2
3
4
5
6
7
addDep (dep) {
var id = dep.id;
if (!this.depIds.has(id)) { // 一次求值时候的去重
this.deps.push(dep);
dep.addSubs(this); // 再将 watcher 放到字段的 Dep 的实例 subs 中, 以供值变化的时候 执行 notity, 把 subs 中的 watcher 拉出来挨个执行一遍
}
}

How to execute callbacks when triggering setter?

When setter is triggered, it retrieves the array containing watcher for that property and executes each one sequentially:

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
// setter
set (newVal) {
// 先触发一次依赖, 在 watcher 中 维护一个 depId 去重
if (newVal === val) {
console.log('值相等, 未变化, 呵呵');
return;
}
val = newVal;
dep.notity(); // 触发 值 watcher 的 update 操作
}
// dep.notity() 函数:
notity () {
this.subs.forEach((watcher) => {
watcher.update();
});
},
// watcher.update() 函数:
update () { // 触发值变化的时候, 执行该函数进行求值及更新操作
var value = this.get();
if (value !== this.value) {
console.log(`恭喜你成功更新了该值, 当前 deps 为: ${this.deps}, depsId 为: ${this.depIds}, 旧值为: ${this.value}, 新值为: ${value}`);
this.cb.call(this.data, this.key);
} else {
console.log('两次相同, 无需更改');
}
}

Specific Code:

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
var wId = 0; // watcher 自增 id
var dId = 0; // dep 自增 id
/**
@for 为了观察 data 上的属性变化
*/
function observe(data) {
var propertys = Object.keys(data);
var dep = new Dep(); // 闭包 Dep 外界不可访问, 实例的 subs 放着它的 watcher
propertys.forEach((property) => {
var val = data[property];
Object.defineProperty(data, property, {
get() {
// 添加依赖到 该属性的 闭包 dep 中
dep.depend();
return val;
},
set(newVal) {
// 先触发一次依赖, 在 watcher 中 维护一个 depId 去重
if (newVal === val) {
console.log('值相等, 未变化, 呵呵');
return;
}
val = newVal;
dep.notity(); // 触发 值 watcher 的 update 操作
},
});
});
}

/**
@for 收集 data 的 key 字段变化时候的响应, 并执行
*/
function Watcher(data, key, cb) {
this.data = data;
this.key = key;
this.cb = cb;
this.depIds = new Set();
this.deps = [];
this.value = this.get(); // 触发取值操作
}

Watcher.prototype = {
update() {
// 触发值变化的时候, 执行该函数进行求值及更新操作
var value = this.get();
if (value !== this.value) {
console.log(
`恭喜你成功更新了该值, 当前 deps 为: ${this.deps}, depsId 为: ${this.depIds}, 旧值为: ${this.value}, 新值为: ${value}`
);
this.cb.call(this.data, this.key);
} else {
console.log('两次相同, 无需更改');
}
},
get() {
var val;
pushTarget(this); // 为了方便, 设置 Dep.target 为当前 watcher 实例, 用完即删
val = this.data[this.key]; // 触发取值操作: 触发 observe 中的 getter => 触发 key 字段的 dep.depend() => 触发 watcher 的 addDep =>
popTarget(); // 用完弹出 后进先出
return val;
},
addDep(dep) {
var id = dep.id;
if (!this.depIds.has(id)) {
// 一次求值时候的去重
this.deps.push(dep);
dep.addSubs(this); // 再将 watcher 放到字段的 Dep 的实例 subs 中, 以供值变化的时候 执行 notity, 把 subs 中的 watcher 拉出来挨个执行一遍
}
},
};

/**
@for 收集 data 的 key 字段变化时候的响应, 并执行
*/
function Dep() {
this.subs = [];
this.id = wId++;
}
Dep.prototype = {
depend() {
Dep.target.addDep(this); // 触发 get 取值的时候, 将当前 dep 实例添加到 watcher 中
},
notity() {
this.subs.forEach((watcher) => {
watcher.update();
});
},
addSubs(watcher) {
this.subs.push(watcher);
},
};

var targetList = [];
function pushTarget(watcher) {
Dep.target = watcher;
targetList.push(watcher);
}
function popTarget() {
targetList.pop();
Dep.target = targetList[targetList.length - 1];
}

// 执行 watcher
var a = {
b: 'c',
};
observe(a);
// 这个 watcher 是用户自己写的 watch
new Watcher(a, 'b', function (data, key) {
console.log('watcher 回调成功执行!');
});

Code Execution Sequence Diagram:

示意图

- EOF -
Originally published at: Vue Reactivity Principle and Implementation - Xheldon Blog