什么是computed
computed
是组件的计算属性,它的含义是依赖于其他状态而生成的状态。
computed的使用方法
computed
有两种创建方式
- 给
computed
函数传递一个getter
方法创建 immutable reactive ref object
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value = 3 // error,因为plusOne是immutable ref obj
- 给
computed
函数传递一个有get
和set
方法的对象来创建一个 writable ref object
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
computed缓存值的原理
熟悉 Vue
的开发者都知道 computed
的特性就在于能够缓存计算的值(提升性能),只有当 computed
的依赖发生变化时才会重新计算,否则读取 computed
的值则一直是之前的值。那么 computed
是怎么做到这一点的呢?为什么 computed
能够知道依赖已经发生了改变需要重新计算呢?当创建一个 computed
是在 Vue
的内部到底发生了什么?让我们一起来看看吧。
创建一个computed
创建 computed
需要调用 computed
函数,computed
函数接收一个 getter
方法或者是一个含有 get
方法和 set
方法的对象,并返回一个 ref
对象。
以下是 computed
工厂函数的全部代码:
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
产生ref对象
通过上面的代码我们发现,Vue
通过执行构造方法ComputedRefImpl
来创建ref对象,哦,我们现在知道了computed
函数返回的ref对象其实就是执行构造方法 ComputedRefImpl
而创建的一个实例。那么能让computed
缓存值的功能一定在ComputedRefImpl
的内部逻辑里了,我们一起接着往下看。
以下是 ComputedRefImpl
的全部代码:
class ComputedRefImpl<T> {
private _value!: T
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
if (self._dirty) {
self._value = this.effect()
self._dirty = false
}
track(self, TrackOpTypes.GET, 'value')
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
我们一起来看 ComputedRefImpl
的构造方法到底做了什么,该构造方法一共做了两件事:
- 调用
effect
方法生成watcher
监听函数并赋值给实例的 effect 属性,(effect方法来自于reactivity/effect.ts) - 设置ref对象是否为
readonly
不过构造方法好像和 computed
的功能扯不上关系,不要急,我们接着往下看。
getter 方法的执行时机
我们发现声明一个 computed
时其实并不会执行getter方法,只有在读取 computed
值时才会执行它的 getter
方法,那么接下来我们就要关注 ComputedRefImpl
的 getter
方法。
getter
方法会在读取 computed
值的时候执行,而在 getter
方法中有一个叫 _dirty
的变量,它的意思是代表脏数据的开关,默认初始化时 _dirty
被设为 true
,在 getter
方法中表示开关打开,需要计算一遍computed
的值,然后关闭开关,之后再获取 computed
的值时由于 _dirty
是 false
就不会重新计算。这就是 computed
缓存值的实现原理。
computed重新计算值
那么 computed
是怎么知道要重新计算值的呢?
其实很简单就是在某个地方将 _dirty
的值设为 true
,获取 computed
的值的时候不就会重新计算了嘛。那么在哪里出现了将 _dirty
设为 true
的操作呢?没错,就是在构造函数里。effect
函数会给对象的响应式对象生成监听函数,并对 scheduler
进行了设置,还记得我们在创建 computed
时传递进来的 getter
方法吗,此方法就是 computed
内部依赖的状态变化时会执行的操作。所以最终的流程就是:computed
内部依赖的状态发生改变,执行对应的监听函数,这其中自然会执行 scheduler
里的操作。而在 scheduler
中将 _dirty
设为了 true
。
也许看到这里有人还会产生一个疑问,computed是怎么知道内部依赖产生了变化呢?这是由于在我们第一次获取computed值(即执行getter方法)的时候对内部依赖进行了访问,在那个时候就对其进行了依赖收集操作,所以computed能够知道内部依赖产生了变化。