前面几节,我们已经学习了很多响应式相关的源码,有点遗忘的朋友可以再回顾一下。
这一节我们继续探究Vue3的源码,来看一下计算属性computed是如何实现的。
computed
基本用法
我们先看一下computed的基本用法:
定义:接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
举例:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误 只含有getter的计算属性,不能修改
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 带set的计算属性,可修改
computed 的使用还是比较简单的,但是我们知道 计算属性 的优点在于它具有 缓存性,不会进行一些无用的重复计算,只有当它 依赖属性 发生变化的时候才会重新计算,这样也就能提高Vue整体的性能。接下来,我们正式从源码层看看它是如何实现的:
computed
function computed(getterOrOptions, debugOptions) {
let getter;
let setter;
// 判断传入的第一个参数是否为函数,如果是函数形式,则说明只有一个getter
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
// 当计算属性没有传入set时,将setter定义为报警函数
setter = () => {
console.warn("Write operation failed: computed value is readonly");
}
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter);
return cRef;
}
computed这个API做的事情,就是根据接到的参数定义 getter 和 setter,如果没有传入 setter,则将setter定义为一个在控制台打印报警信息的函数,最后创建一个 ComputedRefImpl实例,并返回出去。
根据 computed 的定义,我们知道最后返回出去的是一个 ref对象,那么 ComputedRefImpl 是如何创建这个 ref对象 的呢?我们接着往下看:
ComputedRefImpl
var ComputedRefImpl = class {
constructor(getter, _setter, isReadonly) {
// 定义setter方法
this._setter = _setter;
this.dep = void 0;
// 是否为ref类型
this.__v_isRef = true;
// 是否只读
this.__v_isReadonly = false;
// 用于控制是否走缓存值
this._dirty = true;
// 将getter包装为一个副作用函数effect
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
// 触发更新
triggerRefValue(this);
}
});
// 为副作用函数赋computed属性,在触发阶段会优先执行带有computed属性的effect
this.effect.computed = this;
this.effect.active = this._cacheable = true;
this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
}
get value() {
const self = toRaw(this);
// 依赖收集
trackRefValue(self);
// 如果是SSR模式,则_cacheable则默认为false,从而每次get都会重新计算
if (self._dirty || !self._cacheable) {
self._dirty = false;
// 执行副作用函数获取执行结果
self._value = self.effect.run();
}
return self._value;
}
// 修改计算属性
set value(newValue) {
this._setter(newValue);
}
};
上面就是 ComputedRefImpl 的整体逻辑,我们来一步步分析:
- 首先,会将传入的修改方法
setter赋值给自身_setter属性,确定了实例的修改方法set。当计算属性发生修改操作时,直接触发_setter的执行。 - 然后,将 getter函数 包装为 副作用函数effect ,当
getter函数中的 依赖 发生变化时,会触发scheduler调度参数中的函数执行,从而将_dirty赋值为 true,并触发更新。 - 最后,为实例确定 get方法,当我们尝试获取计算属性的值时,会触发get方法的执行,首先会对计算属性进行 依赖收集,然后根据
_dirty值判断是否需要重新计算。如果_dirty为 true,则重新运行副作用函数effect 获取最新值并赋值给_value,并返回出去;如果为 false,则直接返回缓存值。
额外说明:
计算属性的缓存是通过_dirty 和 _cacheable这两个属性来控制的。
_dirty用于控制计算属性的取值是否重新计算,而当我们采用的是SSR模式的话,则会导致 _cacheable属性为true,则每次取值都会涉及重新计算。
整个流程大概如下图:
至此,关于计算属性computed的缓存功能的实现我们大概就了解了。
triggerEffects
再补充一个小的点:在源码中,我们有注意到,computed中在将getter包装为副作用函数effect之后,还为effect赋上了computed属性,这个属性有什么用呢?
这里就要再提一下触发副作用执行的函数:triggerEffects:
// 先执行带有computed属性的副作用函数,再执行普通的副作用函数
function triggerEffects(dep, debuggerEventExtraInfo) {
const effects = isArray(dep) ? dep : [...dep];
for (const effect of effects) {
// 如果副作用函数上有computed属性,则优先执行带有computed属性的副作用函数
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
当同一个依赖发生改变会触发多个副作用函数时,会优先执行带有computed属性的副作用函数。
总结
最后,关于这一节我们学习的计算属性computed做一个总结:
- 首先,computed会根据传入的参数的类型决定该计算属性是否可修改,如果没有传入set方法,则将修改方法
setter定义为报警函数。 - 然后,将
getter函数包装为一个副作用函数effect,当getter的依赖项发生改变时,会触发scheduler函数的执行,将_dirty赋值为true,同时触发更新操作。 - 最后,当我们去获取计算属性
computed的值时,会根据_dirty来决定是否要重新计算:- 当依赖项发生改变的时候,触发
sheduler调度执行,_dirty=true操作,当获取computed的值时就会重新计算; - 依赖项没有发生改变,则不会触发
sheduler调度执行,那么_dirty值为false,计算属性直接返回缓存值。
- 当依赖项发生改变的时候,触发