手把手搭建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 的调度逻辑,实现了数据变化到视图更新的自动联动,且计算属性的更新由其依赖的数据变化驱动,无需手动干预。

相关推荐
歪歪1002 分钟前
Vue原理与高级开发技巧详解
开发语言·前端·javascript·vue.js·前端框架·集成学习
zabr2 分钟前
我让AI一把撸了个算命网站,结果它比我还懂玄学
前端·aigc·ai编程
xianxin_3 分钟前
CSS Fonts(字体)
前端
用户2519162427114 分钟前
Canvas之画图板
前端·javascript·canvas
快起来别睡了30 分钟前
前端设计模式:让代码更优雅的“万能钥匙”
前端·设计模式
EndingCoder1 小时前
Next.js API 路由:构建后端端点
开发语言·前端·javascript·ecmascript·全栈·next.js·api路由
2301_810970391 小时前
wed前端第三次作业
前端
程序猿阿伟1 小时前
《深度解构:React与Redux构建复杂表单的底层逻辑与实践》
前端·react.js·前端框架
酒酿小圆子~1 小时前
【Agent】ReAct:最经典的Agent设计框架
前端·react.js·前端框架
浩星1 小时前
react+vite-plugin-react-router-generator自动化生成路由
前端·react.js·自动化