想起上次面试,被问了个古老的问题:watch 和 computed 的区别。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。上一篇看了watch的源码,本篇针对computed做分析。
一、类型声明
computed的源码在reactivity/src/computed.ts里,先来看看相关的类型定义:
- ComputedRef:调用- computed得到的值的类型,继承自- WritableComputedRef;
- WritableComputedRef:继承自- Ref,拓展了一个- effect属性;
- ComputedGetter:传递给- ComputedRef的构造器函数,用于创建- effect;
- ComputedSetter:传递给- ComputedRef的构造器函数,用于在实例的值被更改时,即在- set中调用;
- WritableComputedOptions:可写的- Computed选项,包含- get和- set,是- computed函数接收的参数类型之一。
| 12
 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;
 
 export interface ComputedRef<T = any> extends WritableComputedRef<T> {
 readonly value: T;
 [ComputedRefSymbol]: true;
 }
 
 
 export interface WritableComputedRef<T> extends Ref<T> {
 readonly effect: ReactiveEffect<T>;
 }
 
 
 export type ComputedGetter<T> = (...args: any[]) => T;
 export type ComputedSetter<T> = (v: T) => void;
 
 
 export interface WritableComputedOptions<T> {
 get: ComputedGetter<T>;
 set: ComputedSetter<T>;
 }
 
 | 
二、ComputedRef
而computed()返回一个ComputedRef类型的值,那么这个ComputedRef就至关重要了。从接口声明中可以看出,它继承了Ref,因而其实现也和Ref较为相似:接收getter、setter等,用getter来创建effect,由effect.run()来获取value,在get中返回;而setter在实例的值更改时,即在set中调用。
| 12
 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> {
 public dep?: Dep = undefined;
 
 
 private _value!: T;
 
 public readonly effect: ReactiveEffect<T>;
 
 public readonly __v_isRef = true;
 public readonly [ReactiveFlags.IS_READONLY]: boolean = false;
 
 public _dirty = true;
 
 public _cacheable: boolean;
 
 
 constructor(
 getter: ComputedGetter<T>,
 
 private readonly _setter: ComputedSetter<T>,
 isReadonly: boolean,
 isSSR: boolean
 ) {
 
 this.effect = new ReactiveEffect(getter, () => {
 if (!this._dirty) {
 this._dirty = true;
 triggerRefValue(this);
 }
 });
 
 this.effect.computed = this;
 this.effect.active = this._cacheable = !isSSR;
 this[ReactiveFlags.IS_READONLY] = isReadonly;
 }
 
 
 get value() {
 
 const self = toRaw(this);
 
 trackRefValue(self);
 if (self._dirty || !self._cacheable) {
 self._dirty = false;
 
 
 self._value = self.effect.run()!;
 }
 return self._value;
 }
 
 
 set value(newValue: T) {
 this._setter(newValue);
 }
 }
 
 | 
三、computed
1. computed的重载签名
computed有两个,主要是接收的第一个参数不同。一是类型为ComputedGetter的函数getter,该函数返回一个值;二是类型为WritableComputedOptions的options,它是一个对象,包含get和set两个函数,作用可以大致理解为与属性描述符里的get和set相似,但不是一回事,只是实现了相似的能力。事实上这个get的作用和第一种重载里的getter完全一致。换句话说,第一种重载没有set只有get,在后续的处理中,会给它包装一个set,只是包装的set只会触发警告。而第二种重载里自带set(由我们写代码时传入),除非我们传入的set是故意用于告警,否则是可以起作用的(通常在其中更新依赖数据的值,尤其是通过emit来告知父组件更新依赖数据)。
| 12
 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,则取出其中的get和set,分别作为getter和setter;
- 用getter和setter创建一个ComputedRef实例并返回该实例。
| 12
 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>;
 
 
 const onlyGetter = isFunction(getterOrOptions);
 if (onlyGetter) {
 getter = getterOrOptions;
 
 setter = __DEV__
 ? () => {
 console.warn("Write operation failed: computed value is readonly");
 }
 : NOOP;
 } else {
 getter = getterOrOptions.get;
 setter = getterOrOptions.set;
 }
 
 
 
 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文件里的规定。
看完这两篇,下次如果还有人问watch和computed的区别这种古董问题,就从源码上逐一比较吧。