手把手搭建Vue轮子从0到1:6. Computed 源码解读

上一章:手把手搭建Vue轮子从0到1:5. Ref 模块的实现

计算属性 computed 会 基于响应式依赖被缓存,并且在依赖的响应式数据发生变化时 重新计算

创建测试实例:

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>computed</title>
    <script src="../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    const { effect, reactive, computed } = Vue

    const obj = reactive({
      count: 0,
    })

    const computedObj = computed(() => {
      return obj.count * 2
    })

    effect(() => {
      document.querySelector('#app').innerHTML =
        `count is: ${computedObj.value}`
    })

    setTimeout(() => {
      obj.count++
    }, 1000)
  </script>
</html>

在上面的测试实例中,程序主要执行了 5 个步骤:

  1. 使用 reactive 创建响应性数据
  2. 通过 computed 创建计算属性 computedObj,并且触发了 obj 的 getter
  3. 通过 effect 方法创建了 fn 函数
  4. 在 fn 函数中,触发了 computed 的 getter
  5. 延迟触发了 obj 的 setter

阶段 1:初始化响应式数据与计算属性

  1. const obj = reactive({ count: 0 })
  • 源码对应 packages/reactivity/src/reactive.ts 的 reactive 函数:

    • 为 obj 创建响应式代理(Proxy),代理拦截 get(用于依赖收集)和 set(用于触发更新)操作。
    • 内部通过 createReactiveObject 函数,为对象添加 __v_isReactive 标记(标识为响应式对象),并使用 baseHandlers 作为代理处理器。
  1. const computedObj = computed(() => obj.count * 2)
  • 执行 computed 函数(packages/reactivity/src/computed.ts):

    • 检测到传入的是函数(getter),创建 ComputedRefImpl 实例(cRef)。
    • ComputedRefImpl 构造函数核心逻辑:
      • 创建 ReactiveEffect 实例(this.effect),传入两个参数:
        • getter:() => obj.count * 2(计算逻辑)。
        • scheduler:() => triggerRefValue(this)(依赖变化时的调度函数)。
      • 关联 effect 与计算属性:this.effect.computed = this。

阶段 2:注册 effect 副作用

  1. effect(() => { document.querySelector('#app').innerHTML = ... })
  • 执行 effect 函数(packages/reactivity/src/effect.ts):
    • 创建 ReactiveEffect 实例(记为 renderEffect),传入回调函数(更新 DOM 的逻辑)。
    • 执行 effect.run() 方法,触发回调函数执行(初始化 DOM):
      • 回调中访问 computedObj.value,触发 ComputedRefImpl 的 get value 方法:
        • 执行 trackRefValue(this)(packages/reactivity/src/ref.ts):
          • 收集依赖:将当前活跃的 renderEffect 记录到 computedObj 的依赖列表(deps)中,后续 computedObj 变化时会触发该 effect。
        • 执行 this.effect.run()(ReactiveEffect 的 run 方法):
          • 执行计算属性的 getter(obj.count * 2),此时访问 obj.count,触发响应式代理的 get 拦截器:
            • 执行 track 函数(packages/reactivity/src/effect.ts):收集依赖,将 computedObj 的 effect(记为 computedEffect)记录到 obj.count 的依赖列表中
          • 计算结果(0 * 2 = 0)赋值给 this._value,并返回。
      • 最终 DOM 被更新为 count is: 0。

阶段 3:1 秒后更新数据(setTimeout 逻辑)

  1. setTimeout(() => { obj.count++ }, 1000)
  • 1 秒后执行回调,触发 obj.count 的更新:
    • obj.count++ 触发响应式代理的 set 拦截器(baseHandlers 中的 set 方法):
      • 执行 trigger 函数(packages/reactivity/src/effect.ts):找到 obj.count 依赖列表中的 computedEffect(计算属性的 effect)。

触发计算属性的调度器

  • trigger 函数内部调用 triggerEffect 处理 computedEffect:
    • 检测到 computedEffect 存在 scheduler(即 ComputedRefImpl 中定义的 () => triggerRefValue(this)),执行该调度器:
      • triggerRefValue(computedObj)(packages/reactivity/src/ref.ts):遍历 computedObj 的依赖列表(即 renderEffect),调用 triggerEffect(renderEffect)。

触发 DOM 更新

  • triggerEffect(renderEffect) 执行 renderEffect.run():
    • 重新执行 effect 回调(更新 DOM),再次访问 computedObj.value: - 触发 ComputedRefImpl 的 get value,执行 this.effect.run():
      • 重新执行计算属性的 getter(此时 obj.count 已变为 1,计算结果为 2)。
    • DOM 被更新为 count is: 2。

核心流程总结

  1. 初始化:reactive 创建响应式代理,computed 通过 ComputedRefImpl 关联 ReactiveEffect(含计算逻辑和调度器)。

  2. 依赖收集:

    2.1 effect 执行时,通过 computedObj.value 的 get 触发 trackRefValue,将 renderEffect 收集为 computedObj 的依赖。

    2.2 计算属性的 getter 执行时,通过 obj.count 的 get 触发 track,将 computedEffect 收集为 obj.count 的依赖。

  3. 数据更新:

    3.1. obj.count++ 触发 set 拦截器,通过 trigger 找到 computedEffect 并执行其调度器。

    3.2. 调度器通过 triggerRefValue 触发 renderEffect,重新执行回调更新 DOM。

整个过程通过 依赖收集(track) 和 更新触发(trigger) 机制,结合 ReactiveEffect 的调度逻辑,实现了数据变化到视图更新的自动联动,且计算属性的更新由其依赖的数据变化驱动,无需手动干预。

相关推荐
Object~4 分钟前
4.const和iota
开发语言·前端·javascript
小小小小宇6 分钟前
前端监测界面内存泄漏
前端
掘金安东尼6 分钟前
⏰前端周刊第 448 期(2026年1月4日-1月10日)
前端·面试·github
邹阿涛涛涛涛涛涛8 分钟前
月之暗面招聘 Android
面试·招聘
攀登的牵牛花10 分钟前
前端向架构突围系列 - 工程化(一):JavaScript 演进史与最佳实践
前端·javascript
夏天想13 分钟前
为什么使用window.print打印的页面只有第一页。其他页面没有了。并且我希望打印的是一个弹窗的内容,竟然把弹窗的样式边框和打印的按钮都打印进去了
前端·javascript·html
FinClip15 分钟前
凡泰极客FinClip荣获2025中国企业IT大奖!AI+超级APP重塑企业AI服务
前端·架构·openai
一起努力啊~18 分钟前
算法刷题--哈希表
算法·面试·散列表
小酒星小杜22 分钟前
在AI时代下,技术人应该学会构建自己的反Demo地狱系统
前端·vue.js·ai编程
Code知行合壹32 分钟前
Pinia入门
vue.js