想起上次面试,被问了个古老的问题: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函数接收的参数类型之一。
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较为相似:接收getter、setter等,用getter来创建effect,由effect.run()来获取value,在get中返回;而setter在实例的值更改时,即在set中调用。
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,该函数返回一个值;二是类型为WritableComputedOptions的**options,它是一个对象,包含get和set两个函数,作用可以大致理解为与属性描述符里的get和set相似**,但不是一回事,只是实现了相似的能力。事实上这个get的作用和第一种重载里的getter完全一致。换句话说,第一种重载没有set只有get,在后续的处理中,会给它包装一个set,只是包装的set只会触发警告。而第二种重载里自带set(由我们写代码时传入),除非我们传入的set是故意用于告警,否则是可以起作用的(通常在其中更新依赖数据的值,尤其是通过emit来告知父组件更新依赖数据)。
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实例并返回该实例。
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文件里的规定。
看完这两篇,下次如果还有人问watch和computed的区别这种古董问题,就从源码上逐一比较吧。