Vueのリアクティブ原理と実装

✍🏼 作成日 2016年08月02日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください

本文の背景

Vueのリアクティブシステムの原理について研究し、それを記録するとともに、Vueのリアクティブ機能を自分で実装してみようと思い、本記事を執筆しました。

概要

Vue リアクティブデータの初期化は、initStateinitDataで行われます。observe関数を使用してvmオブジェクトのdataプロパティを監視し、その後gettersetterプロパティを設定します。
したがって、少なくとも3つの関数が必要です:プロパティgettersetterを監視するオブザーバー、gettersetterをトリガーする监听者、およびプロパティの依存関係を保存する收集框です。これらをそれぞれobservewatcherdepと命名しました。

処理フロー

まずobserveにおいて、data内のプロパティに再帰的にgettersetterを追加します。その後、watcherのいずれかのプロパティにアクセスすると、watcherインスタンスがgetterインターセプターをトリガーし、そのプロパティのインターセプターがwatcherインスタンスをそのプロパティのクロージャ依存コレクターdepに追加します。同時に、このクロージャ依存コレクターをwatcherに格納します。

その後、上記のプロパティを変更すると、observeで定義されたsetterインターセプターがトリガーされ、この時dep内のwatcherが動作を開始し、watcherのコールバックを実行します。

注意事項

getterがトリガーされた際に依存関係が重複して収集されないようにするには?

watchergetterをトリガーする際、現在のプロパティの依存コレクターをwatcherのSet配列に追加して比較し、重複している場合は追加しません:

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 拉出来挨个执行一遍
}
}

setterがトリガーされた際にコールバックを実行するには?

setterがトリガーされると、そのプロパティに含まれる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
// 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('两次相同, 无需更改');
}
}

具体的なコード:

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 回调成功执行!');
});

コード実行順序の図解:

示意图

- EOF -
この記事の初出: Vueのリアクティブ原理と実装 - Xheldon Blog