什么是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能够知道内部依赖产生了变化。