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

相关推荐
泽虞41 分钟前
《LINUX系统编程》笔记p8
linux·运维·服务器·c语言·笔记·面试
网络研究院4 小时前
苹果 Safari 地址栏可能被超大光标视觉欺骗
前端·safari·苹果
slongzhang_5 小时前
html添加水印
前端·html
软件测试媛5 小时前
14:00面试,15:00就出来了,问的问题过于变态了。。。
面试·职场和发展
Small black human5 小时前
前端-什么是Vue
前端·javascript·vue.js
IT 前端 张6 小时前
Axios与Ajax:现代Web请求大比拼
前端·javascript·ajax
ikun778g7 小时前
uniapp使用uview UI,自定义级联选择组件
前端·前端框架·uni-app
java水泥工7 小时前
基于Echarts+HTML5可视化数据大屏展示-惠民服务平台
前端·echarts·html5
计算机学姐7 小时前
基于SpringBoot的运动服装销售系统【2026最新】
java·vue.js·spring boot·后端·spring·tomcat·mybatis
007php0077 小时前
Go 面试题: new 和 make 是什么,差异在哪?
后端·算法·docker·容器·面试·职场和发展·golang