Vue3 响应式(4)-Wacth 的实现原理

前几篇文章已经构建了较为完善的响应式系统,并且实现了计算属性 computed 以及调度器、懒执行等特性。这一篇文章就来实现另外一个重要的功能—— watch 监听。

watch 的基础原理

简单实现

watch 无非是实现这样一个功能:对于指定的响应式变量,当该变量发生变化时能够执行指定的回调函数。这一个需求完全可以使用调度器 scheduler 来实现。例如下面的代码,通过给 effect 传入一个读取 data.cnt 的函数,来使 data.cnt 与副作用函数进行绑定。然后在 scheduler 上指定回调函数,每当 data.cnt 发生变化时就执行指定的回调函数。

1
2
3
4
5
6
7
8
9
10
const data = reactive({ cnt: 0 })

effect(() => {
data.cnt // 触发 track 函数
}, {
scheduler: () => {
// to do something when data.cnt changes
console.log('data.cnt changed')
}
})

递归监听所有属性

上一节中实现监听的方式有一个明显的不足,那就是必须指定要监听响应式数据的哪个属性,否则其他属性的变化不会触发绑定的回调函数。但实际上我们希望所监听变量的任何属性发生变化都能触发回调。这就自然而然想到使用递归的方法来遍历变量的所有属性。我们如此封装 watch 函数,其中 traverse(obj) 用来递归的读取 obj 的所有属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function watch(obj, handler) {
effect(
() => traverse(obj),
{
scheduler: hanlder
}
)
}

function traverse(obj, seen = new Set()) {
if (typeof obj !== 'object' || value === null || seen.has(obj)) return
seen.add(obj)

for (const k in obj) {
traverse(obj[k])
}

return obj // 这里之所以返回 obj 是为了后面保存 watch 的值用
}

有了以上的封装我们就可以直接调用 watch 来自动追踪响应式数据的所有变化了:

1
2
3
4
watch(obj, () => {
// to do something when data changes
console.log('obj changed')
})

getter

Vue 中对于 watch 方法还提供了 getter 的使用方式,它的调用方式如下。通过 getter 返回一个响应式数据,watch 仅仅监听 getter 返回的值,并且默认不会进行深层监听。

1
2
3
4
5
6
watch(
() => obj.cnt,
() => {
// to do something when data changes
}
)

watch 添加 getter 的实现非常简单,即通过判断 watch 的第一个参数是不是函数 (getter),如果是函数则直接将 getter 作为副作用函数传给 effect,否则使用上面的递归处理。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function watch(source, handler) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(
getter,
{
scheduler: hanlder
}
)
}

watch 的功能优化

获得“新旧值”

这里实现 Vue.js 中回到函数中获取新旧值参数的功能。这里主要利用到 lazy 选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function watch(source, handler) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}

let oldVal, val

const effectFn = effect(
getter,
{
lazy: true,
scheduler() {
val = effectFn()
handler(oldVal, val)
oldVal = val
}
}
)

oldVal = effectFn()
}

immediate 立即执行

通过以上的封装,watch 所监听的数据只有发生变化时才会执行回调函数。我们同样可以像 Vue.js 中为其设置 immediate 参数来使其立即执行。

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
function watch(source, handler, options) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}

let oldVal, val

const job = () => {
val = effectFn()
handler(oldVal, val)
oldVal = val
}

const effectFn = effect(
getter,
{
lazy: true,
scheduler: job
}
)

if (options.immediate) {
job()
} else {
oldVal = effectFn()
}
}

小结

本文实现了 Vue.js 数据侦听方法 watch 的基础原理,全部的代码如下。点击这里 在线运行和调试。

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
let activeEffect;
const effectStack = [];
const bucket = new WeakMap();

const data = reactive({ foo: 1, bar: 2 });

watch(
() => data.bar,
(old, val) => {
console.log(`data change from ${old} to ${val}`);
},
{ immediate: true }
);

setTimeout(() => {
data.bar = 100;
}, 2000);

function watch(source, cb, options = {}) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}

let oldVal, val;
const job = () => {
val = effectFn();
cb(oldVal, val);
oldVal = val;
};

const effectFn = effect(getter, {
lazy: true,
scheduler: job
});

if (options.immediate) {
job();
} else {
oldVal = effectFn();
}
}

function traverse(obj, seen = new Set()) {
if (typeof obj !== "object" || obj === null || seen.has(obj)) return;
for (const k in obj) {
traverse(obj[k]);
}
return obj;
}

function computed(getter) {
let dirty = true;
let cache;
const effectFn = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true;
trigger(obj, "value");
}
});
const obj = {
get value() {
if (dirty) {
cache = effectFn();
dirty = false;
}
track(obj, "value");
return cache;
}
};
return obj;
}

function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
});
}

function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.depSets.push(deps);
}

function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
if (!deps) return;
const depsToRun = new Set(deps);
// depsToRun.forEach(fn => fn())
depsToRun.forEach((fn) => {
if (fn === activeEffect) return;
if (fn.options.scheduler) {
// 如果设置了调度器,则将副作用函数的执行交给调度器
fn.options.scheduler(fn);
} else {
// 否则默认直接执行副作用函数
fn();
}
});
}

function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);

const res = fn(); // 获取副作用函数的结果

effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];

return res; // 返回副作用函数结果
};
effectFn.depSets = [];
effectFn.options = options;
if (!options.lazy) {
effectFn(); // 立即执行一次 effectFn 建立绑定
}
return effectFn; // 返回 effectFn
}

function cleanup(effectFn) {
effectFn.depSets.forEach((deps) => {
deps.delete(effectFn);
});
effectFn.depSets.length = 0;
}

参考资料

[1] vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.[EB/OL]. [2023-09-22]. https://github.com/vuejs/core.

[2] Vue.js - 渐进式 JavaScript 框架 | Vue.js[EB/OL]. [2023-09-22]. https://cn.vuejs.org/.

[3] 霍春阳. Vue.js设计与实现[M/OL]. 人民邮电出版社, 2022[2023-09-19]. https://book.douban.com/subject/35768338/.

[4] JavaScript | MDN[EB/OL]. 2023-04-10. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript.