0%

vue-composition-api源码分析之watch与computed

上一篇基本上搞懂了怎么定义响应式对象的,这里研究一下怎么触发侦测的

api/watch.ts

watch

之前比较疑惑的是,为什么 watch 的第一个参数需要传递一个 getter 或者 ref,而不能传递 reactive.prop
其实这里是调用了 Vue.$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
// @vue/composition-api/src/apis/watch.ts
const stop = vm.$watch(getter, callback, {
immediate: options.immediate,
deep: deep,
sync: isSync,
});

// vue/src/core/instance/state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}

// vue/src/core/observer/watcher.js
export default class Watcher {
// ... 省略各种定义

constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省略无关内容
// parse expression for getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
// 开发环境下警告 省略
}
}
this.value = this.lazy ? undefined : this.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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
}

可以看到 $watch 要求传入 getter 或者一个路径字符串,这个 getter 是要在进行依赖收集的时候被调用的
composition-api 中的 watch 使用了 vue2 自身的 watch 机制

至于 watch 的响应式原理,大致如下吧

watch

通过看源码也搞清了一个点,这里的 childObj 和深度监听一点关系都没有

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
// vue/src/core/observer/index.js
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 无关代码 略过
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);
}
}
}
return value;
},
// 无关代码 略过

它的作用是:每个 Observer 对象都有一个 dep 属性,如果使用 Vue.$set 给对象动态添加新属性,会给赋予该属性响应式并调用该对象的 dep 的 notify

看下面的例子应该就可以完全理解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
data() {
return {
ttt: {
a: 1,
},
};
},
mounted() {
setTimeout(() => {
this.ttt.a = 3; // 无法触发 watch
this.$set(this.ttt, "a", 5); // 无法触发 watch 因为不是新属性,直接赋值后就返回了
this.$set(this.ttt, "b", 5); // 可以触发,因为 ttt 的 __ob__ 上的 dep 调用了 notify
this.ttt = {}; // 可以触发
}, 1000);
},
watch: {
ttt(val) {
console.log("watch", val);
},
},
};

那么看一下深度监听是怎么实现的

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
// vue/src/core/observer/watcher.js
class Watcher {
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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
}

export function traverse(val: any) {
_traverse(val, seenObjects);
seenObjects.clear();
}

function _traverse(val: any, seen: SimpleSet) {
let i, keys;
const isA = Array.isArray(val);
if (
(!isA && !isObject(val)) ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return;
}
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
}

就是递归遍历,深度访问对象所有的值,触发对象所有值的依赖收集。
还有一个点,Vue.$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
// vue/src/core/instance/state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 省略
return function unwatchFn() {
watcher.teardown();
};
};
// vue/src/core/observer/watcher.js
class Watcher {
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}

// vue/src/core/observer/dep.js
class Dep {
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
}

// vue/src/shared/util.js
/**
* Remove an item from an array.
*/
export function remove(arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}

就是把 watcher 从所有依赖的属性的 subs 中删除

computed

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
// @vue/composition-api/src/apis/computed.ts
export function computed<T>(
options: Option<T>["get"] | Option<T>
): ComputedRef<T> | WritableComputedRef<T> {
const vm = getCurrentInstance()?.proxy;
let get: Option<T>["get"], set: Option<T>["set"] | undefined;
if (typeof options === "function") {
get = options;
} else {
get = options.get;
set = options.set;
}

let computedSetter;
let computedGetter;

if (vm && !vm.$isServer) {
const { Watcher, Dep } = getVueInternalClasses();
let watcher: any;
computedGetter = () => {
if (!watcher) {
watcher = new Watcher(vm, get, noopFn, { lazy: true });
}
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
};

computedSetter = (v: T) => {
if (set) {
set(v);
}
};
} else {
// fallback
const computedHost = defineComponentInstance(getVueConstructor(), {
computed: {
$$state: {
get,
set,
},
},
});

vm && vm.$on("hook:destroyed", () => computedHost.$destroy());

computedGetter = () => (computedHost as any).$$state;
computedSetter = (v: T) => {
(computedHost as any).$$state = v;
};
}

return createRef<T>(
{
get: computedGetter,
set: computedSetter,
},
!set
);
}

通过 createRef 传递 get 和 set 创建了 Ref。
computed 是惰性的(初始时 lazy 和 dirty 都为 true),只有在被求值的时候才会计算,然后进行相应的依赖收集

1
2
3
4
5
6
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}

这里有点难理解的,其实是对应了这种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
name: "App",
data() {
return {
t: 5,
};
},
mounted() {
setTimeout(() => {
this.t = 10;
console.log(this.tttt);
}, 1000);
},
computed: {
tt() {
return this.t;
},
ttt() {
return this.tt;
},
},
};

当 computed 依赖于另外的 computed 时,访问 t 的 getter 只会给 tt 的 watcher 添加订阅,但是我们想要的给 tt 和 ttt 的 watcher 都添加订阅
Vue 对于 Dep.target 使用了一个栈去管理,而这种嵌套的 computed 的访问也是栈式的。
访问 ttt 的 getter 时,执行 ttt 的 watcher 的 evaluate(),ttt 的 watcher 入栈,然后嵌套访问 tt 的 getter,然后执行 tt 的 watcher 的 evaluate(),tt 的 watcher 入栈。tt 这一层的 evaluate() 执行完毕后 tt 的 watcher 退栈,这时 Dep.target 是指向 ttt.watcher 的,执行 watcher.depend(),就可以给 ttt 的 watcher 添加订阅了。