手写 vue 源码 === computed 实现

1 阅读7分钟

手写 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 时,会发生以下步骤:

  1. 检查 effect.dirty 是否为 true
  2. 如果是 true,调用 effect.run() 执行 getter 函数
  3. effect.run() 中,会将当前的 effect 设置为 activeEffect
  4. 执行 getter 函数,此时会访问响应式数据,触发这些数据的 get 拦截器
  5. 在 get 拦截器中,会调用   track  函数,将当前的 activeEffect(即计算属性的 effect)收集为依赖
  6. 执行完 getter 后,会将计算结果存储在   _value 中
  7. 调用 trackRefValue(this) ,如果当前有活跃的 effect(比如在渲染函数中使用了这个计算属性),会将这个 effect 收集为计算属性的依赖
3. 嵌套 effect 的处理

计算属性的一个复杂之处在于它涉及到嵌套的 effect:

  1. 计算属性本身是一个 effect(ComputedRefImpl 中的 this.effect)
  2. 使用计算属性的地方(如渲染函数)也是一个 effect

这种嵌套关系通过 activeEffect 变量和 trackRefValue 函数来处理:

export function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(
      activeEffect,
      (ref.dep = ref.dep || createDep(() => (ref.dep = undefined), "undefined"))
    );
  }
}

当我们在一个 effect 中访问计算属性时,这个 effect 会被收集为计算属性的依赖。

4. 更新过程

当计算属性依赖的响应式数据发生变化时:

  1. 触发响应式数据的 set 拦截器
  2. 在 set 拦截器中,调用   trigger  函数,触发所有依赖于这个数据的 effect
  3. 其中包括计算属性的 effect,它会执行调度器函数
  4. 调度器函数调用 triggerRefValue(this) ,触发所有依赖于计算属性的 effect
  5. 同时,计算属性的   dirty 标志会被设置为 true,表示需要重新计算
export function triggerRefValue(ref) {
  let dep = ref.dep;
  if (dep) {
    triggerEffects(dep);
  }
}

嵌套 effect 关系图解

计算属性涉及两层 effect 嵌套:

外层effect (如渲染函数)
  ↓ 访问计算属性
计算属性 (ComputedRefImpl)
  ↓ 内部包含一个effect
计算属性的effect (ReactiveEffect)
  ↓ 访问响应式数据
响应式数据 (如reactive对象)
依赖关系建立过程
  1. 响应式数据 → 计算属性effect : 当计算属性的 getter 执行时,会访问响应式数据,此时响应式数据会收集计算属性的 effect 作为依赖
  2. 计算属性 → 外层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 时:

  1. 检查 dirty 标志,如果为 true 则需要重新计算
  2. 调用 effect.run() 执行 getter 函数,此时会建立 "响应式数据 → 计算属性effect" 的依赖关系
  3. 调用 trackRefValue(this) 收集当前活跃的外层 effect,建立 "计算属性 → 外层effect" 的依赖关系
3. 响应式数据变化时

当响应式数据变化时:

  1. 触发收集的依赖,包括计算属性的 effect
  2. 计算属性的 effect 不会立即重新计算,而是将 dirty 设为 true
  3. 然后调用调度器函数 triggerRefValue(this),触发依赖于计算属性的外层 effect
具体流程示例

假设有以下代码:

const count = ref(1)
const double = computed(() => count.value * 2)
 
effect(() => {
  console.log(double.value) // 访问计算属性
})
 
// 修改count
count.value = 2

执行流程:

  1. 创建 computed 实例,内部创建一个 ReactiveEffect 实例
  2. 执行外层 effect,此时 activeEffect 为这个外层 effect
  3. 访问 double.value,检查 dirty 为 true,执行 effect.run()
  4. 在 run() 中,将计算属性的 effect 设为 activeEffect
  5. 执行 getter 函数,访问 count.value,此时 count 收集计算属性的 effect 作为依赖
  6. getter 执行完毕,恢复 activeEffect 为外层 effect
  7. 调用 trackRefValue(this),计算属性收集外层 effect 作为依赖
  8. 修改 count.value,触发依赖更新
  9. 计算属性的 effect 被触发,将 dirty 设为 true,并调用调度器函数
  10. 调度器函数触发依赖于计算属性的外层 effect
  11. 外层 effect 重新执行,再次访问 double.value,发现 dirty 为 true,重新计算
关键点总结
  1. 双层 effect 结构 :计算属性自身是一个 effect,同时也可以被外层 effect 依赖
  2. 脏值检查机制 :通过 dirty 标志控制是否需要重新计算
  3. 延迟计算 :只有在访问计算属性时才会执行计算
  4. 依赖传递 :响应式数据变化 → 计算属性标记为脏 → 触发外层 effect → 重新访问计算属性 → 重新计算

这种设计使计算属性既能响应依赖变化,又能避免不必要的计算,是 Vue 响应式系统中的一个精巧设计。

计算属性的缓存机制

计算属性的一个重要特性是缓存机制。这是通过 dirty 标志实现的:

  1. 初始时,  dirty  为 true,表示需要计算
  2. 第一次访问计算属性时,执行计算并将结果缓存,然后将   dirty  设为 false
  3. 后续访问时,如果   dirty  为 false,直接返回缓存的结果
  4. 只有当依赖的数据变化时,才会将   dirty  设为 true,表示缓存失效

总结

Vue 的计算属性实现是一个精巧的设计,它通过 ReactiveEffect 类实现了依赖追踪和更新触发。计算属性本身是一个 effect,它会收集自身依赖的响应式数据;同时,计算属性也可以被其他 effect 收集为依赖。

这种双向的依赖关系使得计算属性能够在依赖变化时自动更新,并且只在必要时重新计算,从而提高了性能。理解计算属性的实现原理,有助于我们更好地使用 Vue,并在遇到复杂场景时能够更好地排查问题。