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

相关推荐
卡布叻_星星4 小时前
前端JavaScript笔记之父子组件数据传递,watch用法之对象形式监听器的核心handler函数
前端·javascript·笔记
开发加微信:hedian1165 小时前
短剧小程序开发全攻略:从技术选型到核心实现(前端+后端+运营干货)
前端·微信·小程序
徐小夕@趣谈前端6 小时前
如何实现多人协同文档编辑器
javascript·vue.js·设计模式·前端框架·开源·编辑器·github
YCOSA20257 小时前
ISO 雨晨 26200.6588 Windows 11 企业版 LTSC 25H2 自用 edge 140.0.3485.81
前端·windows·edge
小白呀白7 小时前
【uni-app】树形结构数据选择框
前端·javascript·uni-app
吃饺子不吃馅7 小时前
深感一事无成,还是踏踏实实做点东西吧
前端·svg·图形学
90后的晨仔8 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端
PAK向日葵8 小时前
【算法导论】一道涉及到溢出处理的笔试题
算法·面试
无敌最俊朗@8 小时前
Qt 自定义控件(继承 QWidget)面试核心指南
开发语言·qt·面试