第六章节 响应式的 computed 实现【手摸手带你实现一个vue3】

大家好,我是作曲家种太阳,今天我们来手把手实现 Vue 3 响应式系统中非常核心的一部分:计算属性 computed。

Vue 3 中的 computed 是基于 Proxy 与 effect 实现的具有缓存能力的响应式属性,它的底层实现包含响应依赖收集、缓存控制与调度器机制等关键模块。下面我们一步一步来实现并理解它的原理。


💡 1. computed 的作用与特性

计算属性是基于其依赖响应式数据自动更新的"派生值",特点是:

  • 懒执行:只有访问时才会执行
  • 缓存机制:依赖没变就不会重新计算
  • 响应式:依赖变化后会重新计算并更新依赖方

🔧 2. 实现目标与步骤规划

我们将按照以下步骤构建 computed

  1. 创建 ComputedRefImpl
  2. 实现 .value 的 getter,支持收集依赖
  3. 利用 ReactiveEffect 实现计算逻辑
  4. 引入 _dirty 控制缓存失效与更新
  5. 引入 scheduler 实现依赖触发逻辑
  6. 集成测试并解决死循环问题

🧱 3. 创建 ComputedRefImpl 类

文件路径:packages/reactivity/src/computed.ts

js 复制代码
 
/**
 * 计算属性类
 */
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true

  /**
   * 脏:为 false 时,表示需要触发依赖。为 true 时表示需要重新执行 run 方法,获取数据。即:数据脏了
   */
  public _dirty = true

  constructor(getter) {
    this.effect = new ReactiveEffect(getter, () => {
      // 判断当前脏的状态,如果为 false,表示需要《触发依赖》
      if (!this._dirty) {
        // 将脏置为 true,表示
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
  }

  get value() {
    // 收集依赖
    trackRefValue(this)
    // 判断当前脏的状态,如果为 true ,则表示需要重新执行 run,获取最新数据
    if (this._dirty) {
      this._dirty = false
      // 执行 run 函数
      this._value = this.effect.run()!
    }

    // 返回计算之后的真实值
    return this._value
  }
}

/**
 * 计算属性
 */
export function computed(getterOrOptions) {
  let getter

  // 判断传入的参数是否为一个函数
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    // 如果是函数,则赋值给 getter
    getter = getterOrOptions
  }

  const cRef = new ComputedRefImpl(getter)

  return cRef as any
}

我们将 getter 包装成 ReactiveEffect,并在 .value 中执行 run() 以计算值,并通过 _dirty 控制缓存是否失效。


🎯 4. track 与 trigger 的联动机制

computed 本质上也是一个 ref,它有自己的 dep,并通过 trackRefValue()triggerRefValue() 实现依赖收集和更新触发。

js 复制代码
export function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

💡 这样 computed 的依赖就能被 effect() 正确收集,并在依赖变动时更新。


⚙️ 5. ReactiveEffect 添加 scheduler

调度器 scheduler 是 computed 实现"懒更新"和"多 effect 顺序控制"的关键。

js 复制代码
this.effect = new ReactiveEffect(getter, () => {
  if (!this._dirty) {
    this._dirty = true
    triggerRefValue(this)
  }
})

当依赖变动时(通过 reactive 的 setter -> trigger -> triggerEffect),就会进入 scheduler,标记 _dirty 为 true,等待下一次 .value 访问时重新计算。


🧪 6. 测试 computed 的响应性与缓存能力

测试代码:computed.html

js 复制代码
<script>
  const { reactive, computed, effect } = Vue

  const obj = reactive({ name: '张三' })
  const fullName = computed(() => {
    console.log('computed run')
    return '姓名:' + obj.name
  })

  effect(() => {
    document.querySelector('#app').innerHTML = fullName.value
  })

  setTimeout(() => {
    obj.name = '李四'
  }, 2000)
</script>

观察打印可以发现:

  • 首次渲染执行一次 computed 逻辑
  • 改变依赖后重新计算
  • 多次 .value 访问不会重复执行逻辑(缓存生效)

🧩 7. 避免死循环的处理

computed 的依赖触发若未正确区分顺序可能造成死循环,我们使用两个循环分开 computed 和普通 effect:

js 复制代码
for (const effect of effects) {
  if (effect.computed) {
    triggerEffect(effect)
  }
}
for (const effect of effects) {
  if (!effect.computed) {
    triggerEffect(effect)
  }
}

这样先触发 computed,再触发依赖它的副作用函数,避免了重复依赖触发造成的递归。

验证 computed 方法

文件路径: /packages/reactivity/src/computed-cache.html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<body>
<div id="app"></div>
</body>

<head>
  <meta charset="UTF-8">
  <script src="../../dist/vue.js"></script>
</head>

</html>

<script>
  const { reactive, computed, effect } = Vue

  const obj = reactive({
    name: '张三'
  })

  const computedObj = computed(() => {
    console.log('计算属性执行计算');
    return '姓名:' + obj.name
  })

  effect(() => {
    document.querySelector('#app').innerHTML = computedObj.value
    document.querySelector('#app').innerHTML = computedObj.value
  })

  setTimeout(() => {
    obj.name = '李四'
  }, 2000);
</script>

然后浏览器打开,就看到computed内部已经执行啦

computed 数据更新过程

● 修改依赖响应式数据(例如 obj.name = xxx)

● 会触发 setter → trigger() → triggerEffects()

● 检测 effect 是否是 computed 类型,有 scheduler,则执行 scheduler

● scheduler 中把 dirty = true,并调用 triggerRefValue(this)

● triggerRefValue 再次触发 effect → 重新执行 .value 逻辑

🧩 scheduler 的作用总结

作用 描述
📦 延迟执行 computed 的值只有在下次访问 .value 时才重新计算,而不是数据一改就立即执行 getter
⛔ 避免死循环 避免依赖链中 computed 自己触发自己
⚙️ 控制依赖通知 通过 scheduler 控制什么时候调用 triggerRefValue,通知依赖 effect 重新执行
✅ 配合 _dirty scheduler 会设置 _dirty = true,下次访问 .value 时触发真正的计算

小结

特性 实现方式
缓存 使用 _dirty 标记控制是否重新计算
响应式更新 依赖变化触发 scheduler,重新计算并触发依赖
依赖收集 .valuetrackRefValue() 完成收集
多 effect 区分 computed effect 先触发,普通 effect 后触发

至此,我们就完成了 Vue 3 中 computed 的完整实现逻辑!下一节我们将一起探索 watch 的底层逻辑~


相关推荐
Eliauk__2 分钟前
深入剖析 Vue 双向数据绑定机制 —— 从响应式原理到 v-model 实现全解析
前端·javascript·面试
代码小学僧2 分钟前
Cursor 的系统级提示词被大佬逆向出来了!一起来看看优秀 prompt是怎么写的
前端·ai编程·cursor
MrsBaek6 分钟前
前端笔记-Axios
前端·笔记
洋流9 分钟前
什么?还没弄懂关键字this?一篇文章带你速通
前端·javascript
晴殇i10 分钟前
for...in 循环的坑,别再用它遍历 JavaScript 数组了!
前端·javascript
littleplayer12 分钟前
iOS 单元测试详细讲解-DeepSeek
前端
littleplayer14 分钟前
iOS 单元测试与 UI 测试详解-DeepSeek
前端·单元测试·测试
夜熵16 分钟前
Vue中nextTick()用法
前端·面试
小桥风满袖16 分钟前
Three.js-硬要自学系列15 (圆弧顶点、几何体方法、曲线简介、圆、椭圆、样条曲线、贝塞尔曲线)
前端·css·three.js
洋流17 分钟前
JavaScript事件流机制详解:捕获、冒泡与阻止传播
前端·javascript