本文是 vue3 源码分析系列的第三篇文章,主要介绍 vue3 computed 原理。computed 是 vue3 的一个特性,可以根据其他响应式数据创建响应式的计算属性。计算属性的值会根据依赖的数据变化而自动更新,而且具有缓存机制,提高了性能。在这篇文章中,我们将深入探讨 computed 的实现原理,并通过源码分析来理解其工作机制。
在 vue3 中,computed
的基本用法如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>computed</title>
</head>
<body>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
let { reactive, computed } = VueReactivity;
const state = reactive({
count: 0
});
const double = computed(() => state.count * 2);
console.log(double.value); // 0
setTimeout(() => {
state.count++;
console.log(double.value); // 2
}, 1000);
</script>
</body>
</html>
在上面的例子中,我们首先创建了一个响应式对象 state,然后我们使用 computed 创建了一个计算属性 double。这个计算属性的值是 state.count 的两倍。当 state.count 的值发生变化时,double 的值也会自动更新。
computed 函数的源码如下:
// 定义一个名为computed的函数,该函数接受一个参数getterOrOptions,这个参数可以是计算属性的getter函数,也可以是包含get和set方法的对象
export function computed<T>(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>) {
// 定义两个变量getter和setter,分别用于存储计算属性的getter函数和setter函数
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 判断getterOrOptions是否为函数类型,如果是,说明只传入了getter函数
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 如果只传入了getter函数,将getterOrOptions赋值给getter,同时定义setter函数
getter = getterOrOptions
setter = __DEV__
? () => {
warn(
`Write operation failed: computed value is readonly`
)
}
: NOOP
} else {
// 如果传入了包含get和set方法的对象,将get方法赋值给getter,将set方法赋值给setter
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建一个新的ComputedRefImpl实例,并返回。这个实例接收getter、setter和一个布尔值参数
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
computed 函数接受一个函数或一个对象作为参数,如果是函数,就是计算属性的 getter 函数。如果是对象,就是包含 get 和 set 函数的选项对象。computed 函数会返回一个 ComputedRefImpl 的实例。ComputedRefImpl 的构造函数如下:
ComputedRefImpl 函数的源码如下:
class ComputedRefImpl<T> {
// 定义一个私有变量 _dirty,表示是否需要重新计算值。初始化为 true 表示需要重新计算
private _dirty = true
// 定义一个公共的 reactive effect,用于观察和响应值的变化
public readonly effect: ReactiveEffect<T>
// 定义两个公共的只读属性,用于标识这个引用是否只读和是否是响应式的引用
public readonly __v_isReadonly: boolean
public readonly __v_isRef = true
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
// 创建一个 reactive effect,当值发生变化时触发。如果值没有变化则不触发。
this.effect = effect(getter, {
lazy: true, // 懒加载,只有当需要时才执行
scheduler: () => { // 调度器函数
if (!this._dirty) {
this._dirty = true // 重置 _dirty 为 true,表示需要重新计算
}
}
})
this.effect.computed = this
}
// 定义一个 getter 方法,返回当前的值。
get value() {
if (this._dirty) { // 如果 _dirty 为 true,表示需要被重新计算
this._value = this.effect() // 调用 effect 方法来计算值
this._dirty = false // 设置 _dirty 为 false,表示值已经被计算过
}
return this._value
}
// 定义一个 setter 方法,设置新的值并调用传入的 setter 方法来处理这个新值
set value(newValue: T) {
this._setter(newValue)
}
}
ComputedRefImpl 构造函数内部创建了一个 effect 副作用函数,函数传入了 getter 函数,和一个选项对象。选项对象中有两个属性,一个是 lazy,表示副作用函数是否延迟执行,一个是 scheduler,表示副作用函数的调度器,用于在依赖的数据变化时触发副作用函数的重新执行。
effect 函数的源码如下:
function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions): ReactiveEffect<T> {
// 创建一个新的effect函数,调用run方法执行原始的函数
const _effect = new ReactiveEffect(fn, NOOP, () => {
if (_effect.dirty) {
_effect.run()
}
})
// 如果options中没有设置lazy为true,就立即执行effect函数
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
由于在 ComputedRefImpl 类中定义 effect 函数时,传入的 lazy 为 true 函数并不会自动执行。
当我们访问 double.value 时会触发 ComputedRefImpl 类里面的 get 这个函数,执行 effect 函数。在此过程中会建立属性依赖关系。
执行完之后,将 this._dirty 设置为 false,这一步在后面计算属性是否会重新执行起到关键作用。
scheduler 函数通常用于控制计算属性的更新调度。 函数提供了一种额外的调度控制机制,使你能够更好地控制计算属性的更新时机和逻辑。那么 scheduler 是如何被执行的:
function trigger (target, type, key, newValue?, oldValue?) {
// 此处省略部分代码
for (const effect of effects) {
// computed 表示这是一个计算属性
if (effect.computed) {
// 执行调度器函数
// 重新修改 dirty 的值,标记该计算属性为“脏”状态,表示其值已被修改
effect.options.scheduler()
} else {
effect()
}
}
}
对于 effects 中的每一个 effect,如果它是一个计算属性,那么会调用它的调度器函数来进行更新。否则,直接调用 effect() 来运行副作用。
scheduler 函数,用来在依赖项变化时改变 _dirty 标志位,表示计算属性需要重新计算。
当我们再次访问 double.value 时,由于 this._dirty = true 又会重新执行 get 这个函数,然后更新 double 的值。
通过这个机制使得 vue3 能够精确地控制哪些副作用需要在响应式数据变化时更新,从而提高了性能。
通过对 computed
的源码分析,我们可以看到 vue3 如何实现计算属性的。当我们创建一个计算属性时,vue 会收集这个计算属性的所有依赖项。然后,当这些依赖项发生变化时,vue 会重新计算这个计算属性的值。并通过 _dirty
这个标志位来判断是否需要执行副作用函数并更新 _value
的值。这种机制使得我们可以方便地创建基于其他响应式依赖项的计算属性。