手写 vue 源码 === computed 实现
目录
[TOC]
计算属性是 Vue 中非常重要的特性,它允许我们声明性地计算衍生值。本文将深入探讨 Vue 的计算属性是如何实现的,特别是它如何依赖 ReactiveEffect
来完成响应式更新。
计算属性的基本概念
计算属性本质上是一个可缓存的值,它只有在依赖的响应式数据发生变化时才会重新计算。这种特性使得计算属性非常适合处理复杂的逻辑计算。
计算属性的核心实现
export function computed(getterOrOptions) {
let getter;
let setter;
let onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
setter = () => {};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
computed
函数接收一个 getter 函数或者一个包含 get 和 set 方法的对象,然后返回一个
ComputedRefImpl
实例。这个实例就是我们使用的计算属性。
ComputedRefImpl 类的实现
class ComputedRefImpl {
public _value;
public effect;
public dep;
constructor(public getter, public setter) {
this.effect = new ReactiveEffect(
() => getter(this._value),
() => {
triggerRefValue(this);
}
);
}
get value() {
if (this.effect.dirty) {
this._value = this.effect.run();
trackRefValue(this);
}
return this._value;
}
set value(newValue) {
this.setter(newValue);
}
}
ComputedRefImpl
类是计算属性的核心实现,它包含了三个关键属性:
-
_value
:存储计算结果 -
effect
:一个ReactiveEffect
实例,用于追踪依赖和触发更新 -
dep
:存储依赖于这个计算属性的其他 effect
ReactiveEffect 与计算属性的关系
计算属性的实现依赖于 ReactiveEffect
类。让我们看看 ReactiveEffect
的关键部分:
export class ReactiveEffect {
_trackId = 0;
deps = [];
_depsLength = 0;
_runing = 0;
_dirtyLevel = DirtyLevel.Dirty;
public active = true;
constructor(public fn, public scheduler) {}
public get dirty() {
return this._dirtyLevel === DirtyLevel.Dirty;
}
public set dirty(value) {
this._dirtyLevel = value ? DirtyLevel.Dirty : DirtyLevel.NoDirty;
}
run() {
this._dirtyLevel = DirtyLevel.NoDirty;
if (!this.active) {
return this.fn();
}
let lastEffect = activeEffect;
try {
activeEffect = this;
preCleanEffect(this);
this._runing++;
return this.fn();
} finally {
this._runing--;
postCleanEffect(this);
activeEffect = lastEffect;
}
}
}
计算属性的工作流程
现在,让我们详细分析计算属性的工作流程:
1. 创建计算属性
当我们调用 computed
函数时,会创建一个 ComputedRefImpl
实例。在构造函数中,会创建一个 ReactiveEffect
实例,这个实例有两个关键参数:
- 第一个参数是一个函数,它会调用用户提供的 getter 函数
- 第二个参数是一个调度器函数,当依赖的值发生变化时,会调用这个函数
2. 依赖收集过程
当我们访问计算属性的 value
时,会发生以下步骤:
- 检查
effect.dirty
是否为 true - 如果是 true,调用
effect.run()
执行 getter 函数 - 在
effect.run()
中,会将当前的 effect 设置为 activeEffect - 执行 getter 函数,此时会访问响应式数据,触发这些数据的 get 拦截器
- 在 get 拦截器中,会调用 track 函数,将当前的 activeEffect(即计算属性的 effect)收集为依赖
- 执行完 getter 后,会将计算结果存储在 _value 中
- 调用
trackRefValue(this)
,如果当前有活跃的 effect(比如在渲染函数中使用了这个计算属性),会将这个 effect 收集为计算属性的依赖
3. 嵌套 effect 的处理
计算属性的一个复杂之处在于它涉及到嵌套的 effect:
- 计算属性本身是一个 effect(ComputedRefImpl 中的 this.effect)
- 使用计算属性的地方(如渲染函数)也是一个 effect
这种嵌套关系通过 activeEffect
变量和 trackRefValue
函数来处理:
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(
activeEffect,
(ref.dep = ref.dep || createDep(() => (ref.dep = undefined), "undefined"))
);
}
}
当我们在一个 effect 中访问计算属性时,这个 effect 会被收集为计算属性的依赖。
4. 更新过程
当计算属性依赖的响应式数据发生变化时:
- 触发响应式数据的 set 拦截器
- 在 set 拦截器中,调用 trigger 函数,触发所有依赖于这个数据的 effect
- 其中包括计算属性的 effect,它会执行调度器函数
- 调度器函数调用
triggerRefValue(this)
,触发所有依赖于计算属性的 effect - 同时,计算属性的 dirty 标志会被设置为 true,表示需要重新计算
export function triggerRefValue(ref) {
let dep = ref.dep;
if (dep) {
triggerEffects(dep);
}
}
嵌套 effect 关系图解
计算属性涉及两层 effect 嵌套:
外层effect (如渲染函数)
↓ 访问计算属性
计算属性 (ComputedRefImpl)
↓ 内部包含一个effect
计算属性的effect (ReactiveEffect)
↓ 访问响应式数据
响应式数据 (如reactive对象)
依赖关系建立过程
- 响应式数据 → 计算属性effect : 当计算属性的 getter 执行时,会访问响应式数据,此时响应式数据会收集计算属性的 effect 作为依赖
- 计算属性 → 外层effect : 当外层 effect (如渲染函数) 访问计算属性时,计算属性会收集这个外层 effect 作为依赖
这形成了一个依赖链: 响应式数据 → 计算属性effect → 计算属性 → 外层effect
代码实现分析
1. 创建计算属性
constructor(public getter, public setter) {
this.effect = new ReactiveEffect(
() => getter(this._value), //用户的fn
//当前计算属性依赖的值变化了,我们应该触发effect重新执行
() => {
triggerRefValue(this);
}
);
}
这里创建了计算属性的 effect,它有两个参数:
- 第一个是 getter 函数,用于计算值
- 第二个是调度器函数,当依赖变化时会调用它
2. 访问计算属性
get value() {
if (this.effect.dirty) {
this._value = this.effect.run();
trackRefValue(this);
}
return this._value;
}
当访问计算属性的 value 时:
- 检查 dirty 标志,如果为 true 则需要重新计算
- 调用 effect.run() 执行 getter 函数,此时会建立 "响应式数据 → 计算属性effect" 的依赖关系
- 调用 trackRefValue(this) 收集当前活跃的外层 effect,建立 "计算属性 → 外层effect" 的依赖关系
3. 响应式数据变化时
当响应式数据变化时:
- 触发收集的依赖,包括计算属性的 effect
- 计算属性的 effect 不会立即重新计算,而是将 dirty 设为 true
- 然后调用调度器函数 triggerRefValue(this),触发依赖于计算属性的外层 effect
具体流程示例
假设有以下代码:
const count = ref(1)
const double = computed(() => count.value * 2)
effect(() => {
console.log(double.value) // 访问计算属性
})
// 修改count
count.value = 2
执行流程:
- 创建 computed 实例,内部创建一个 ReactiveEffect 实例
- 执行外层 effect,此时 activeEffect 为这个外层 effect
- 访问 double.value,检查 dirty 为 true,执行 effect.run()
- 在 run() 中,将计算属性的 effect 设为 activeEffect
- 执行 getter 函数,访问 count.value,此时 count 收集计算属性的 effect 作为依赖
- getter 执行完毕,恢复 activeEffect 为外层 effect
- 调用 trackRefValue(this),计算属性收集外层 effect 作为依赖
- 修改 count.value,触发依赖更新
- 计算属性的 effect 被触发,将 dirty 设为 true,并调用调度器函数
- 调度器函数触发依赖于计算属性的外层 effect
- 外层 effect 重新执行,再次访问 double.value,发现 dirty 为 true,重新计算
关键点总结
- 双层 effect 结构 :计算属性自身是一个 effect,同时也可以被外层 effect 依赖
- 脏值检查机制 :通过 dirty 标志控制是否需要重新计算
- 延迟计算 :只有在访问计算属性时才会执行计算
- 依赖传递 :响应式数据变化 → 计算属性标记为脏 → 触发外层 effect → 重新访问计算属性 → 重新计算
这种设计使计算属性既能响应依赖变化,又能避免不必要的计算,是 Vue 响应式系统中的一个精巧设计。
计算属性的缓存机制
计算属性的一个重要特性是缓存机制。这是通过 dirty
标志实现的:
- 初始时, dirty 为 true,表示需要计算
- 第一次访问计算属性时,执行计算并将结果缓存,然后将 dirty 设为 false
- 后续访问时,如果 dirty 为 false,直接返回缓存的结果
- 只有当依赖的数据变化时,才会将 dirty 设为 true,表示缓存失效
总结
Vue 的计算属性实现是一个精巧的设计,它通过 ReactiveEffect
类实现了依赖追踪和更新触发。计算属性本身是一个 effect,它会收集自身依赖的响应式数据;同时,计算属性也可以被其他 effect 收集为依赖。
这种双向的依赖关系使得计算属性能够在依赖变化时自动更新,并且只在必要时重新计算,从而提高了性能。理解计算属性的实现原理,有助于我们更好地使用 Vue,并在遇到复杂场景时能够更好地排查问题。