想起上次面试,被问了个古老的问题:watch 和 computed 的区别。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。上一篇看了watch的源码,本篇针对computed做分析。

一、类型声明

computed的源码在reactivity/src/computed.ts里,先来看看相关的类型定义:

  • ComputedRef:调用computed得到的值的类型,继承自WritableComputedRef
  • WritableComputedRef:继承自Ref,拓展了一个effect属性;
  • ComputedGetter:传递给ComputedRef的构造器函数,用于创建effect
  • ComputedSetter:传递给ComputedRef的构造器函数,用于在实例的值被更改时,即在set中调用;
  • WritableComputedOptions:可写的Computed选项,包含getset,是computed函数接收的参数类型之一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
declare const ComputedRefSymbol: unique symbol;

// ComputedRef的接口,调用computed()得到一个ComputedRef类型的值
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T;
[ComputedRefSymbol]: true;
}

// WritableComputedRef继承了Ref并拓展了一个只读属性effect
export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T>;
}

// ComputedGetter 用于创建 effect , ComputedSetter 对应的值在 ComputedRef 实例中的 set 里调用
export type ComputedGetter<T> = (...args: any[]) => T;
export type ComputedSetter<T> = (v: T) => void;

// 可写的Computed
export interface WritableComputedOptions<T> {
get: ComputedGetter<T>;
set: ComputedSetter<T>;
}

二、ComputedRef

computed()返回一个ComputedRef类型的值,那么这个ComputedRef就至关重要了。从接口声明中可以看出,它继承了Ref,因而其实现也和Ref较为相似:接收gettersetter等,用getter来创建effect,由effect.run()来获取value,在get中返回;而setter在实例的值更改时,即在set中调用。

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
export class ComputedRefImpl<T> {
// dep: 收集的依赖
public dep?: Dep = undefined;

// getter获取的实际值
private _value!: T;
// 一个响应式的effect
public readonly effect: ReactiveEffect<T>;
// __v_isRef 提供给 isRef() 判断实例是否为Ref
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean = false;

public _dirty = true;
// 是否可缓存
public _cacheable: boolean;

// 构造器接收 getter 和 setter ,是否只读,是否出自 SSR
constructor(
getter: ComputedGetter<T>,
// 接收只读的私有的 _setter
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// 用传入的 getter 创建一个 effect
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
// 把 effect 的 computed 属性指回 ComputedRef 实例自身
this.effect.computed = this;
this.effect.active = this._cacheable = !isSSR;
this[ReactiveFlags.IS_READONLY] = isReadonly;
}

// 收集依赖,返回 this._value 的值
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
// 收集Ref
trackRefValue(self);
if (self._dirty || !self._cacheable) {
self._dirty = false;
// effect.run() 会拿到 getter() 的值
// 即_value的值来自于 effect,或者说来自于传入的 getter 的返回值
self._value = self.effect.run()!;
}
return self._value;
}

// 当设置ComputedRef的实例的值时,调用传入的_setter
set value(newValue: T) {
this._setter(newValue);
}
}

三、computed

1. computed的重载签名

computed有两个,主要是接收的第一个参数不同。一是类型为ComputedGetter的函数getter,该函数返回一个值;二是类型为WritableComputedOptionsoptions,它是一个对象,包含getset两个函数,作用可以大致理解为与属性描述符里的getset相似,但不是一回事,只是实现了相似的能力。事实上这个get的作用和第一种重载里的getter完全一致。换句话说,第一种重载没有set只有get,在后续的处理中,会给它包装一个set,只是包装的set只会触发警告。而第二种重载里自带set(由我们写代码时传入),除非我们传入的set是故意用于告警,否则是可以起作用的(通常在其中更新依赖数据的值,尤其是通过emit来告知父组件更新依赖数据)。

1
2
3
4
5
6
7
8
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>;
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>;

2. computed的实现

  • 判断我们传入的第一个参数是getter还是options
  • 如果是getter,则包装一个setter用于开发环境下告警;
  • 如果是options,则取出其中的getset,分别作为gettersetter
  • gettersetter创建一个ComputedRef实例并返回该实例。
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
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>;
let setter: ComputedSetter<T>;

// 判断是getter还是options
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
// 包装setter
setter = __DEV__
? () => {
console.warn("Write operation failed: computed value is readonly");
}
: NOOP;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}

// 创建并返回一个ComputedRef,
// 第三个参数控制是否是只读的ComputedRef实例
const cRef = new ComputedRefImpl(
getter,
setter,
onlyGetter || !setter,
isSSR
);

// 主要是开发环境下调试用
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack;
cRef.effect.onTrigger = debugOptions.onTrigger;
}

return cRef as any;
}

我们知道,在computed里是不允许异步操作的,但是看完了computed的源码,好像也没发现哪里不允许异步操作。确实,单纯就computed的源码来看,它是允许异步操作的,但是computed作为计算属性,大致上是取getter的返回值,return是等不到异步操作结束的。而禁用异步操作的规定是在eslint-plugin-vue这个包中的lib/rules/no-async-in-computed-properties.js文件里的规定。

看完这两篇,下次如果还有人问watchcomputed的区别这种古董问题,就从源码上逐一比较吧。