深入源码设计!Vue3.js核心API——Computed实现原理

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第4.8节的基础上进一步解析,附加了测试的代码运行示例,以及对书籍中提到的ES6中的数据结构及其特点进行阐述,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前置章节:

  1. 深入理解Vue3.js响应式系统基础逻辑
  2. 深入理解Vue3.js响应式系统设计之栈结构和循环问题
  3. 深入理解Vue3.js响应式系统设计之调度执行

核心API

懒执行lazy的effect

经过前置的章节的学习,可以发现设计的effect都是立即执行的,而在4.8节的开始,作者向我们设计了一个可以按意愿时间节点执行的effect,其设计逻辑非常简单,就是在传递effectoptions形参时添加内置lazy属性,然后再在effect函数内部进行判断,如果存在lazy,则返回effectFn给调用者。那么何时何地执行effectFn的决定权就到了调用者的手上

传递lazy代码如下:

effect(
  // 指定了 lazy 选项,这个函数不会立即执行
  () => {
    console.log(obj.foo)
  },
  // options
  {
    lazy: true
  }
)

完善判断lazyeffect代码如下:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非 lazy 的时候,才执行
  if (!options.lazy) {
    // 执行副作用函数
    effectFn()
  }
  // 将副作用函数作为返回值返回
  return effectFn
}

那么现在,就可以使用一个变量去接收返回的effectFn,代码如下:

const effectFn = effect(() => {  
  console.log(obj.foo)  
}, { lazy: true })  
  
// 手动执行副作用函数  
effectFn()

对于手动执行,我们可以使用res去接收调用函数的返回值,那么就需要在effectreturn res,代码又得继续完善:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储到 res 中
    const res = fn() // 新增
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res // 新增
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

那么重点来了,lazy和本节要实现的computed的关系在哪?(建议看完整篇再回过来看这个问题)

原因在于computed内部使用lazy去封装了getter,什么是getter?我们接着继续看

getter

从实现层面上看,gettercomputed接收的实参的昵称,代码如下:

function computed(getter) {...}

MDN的定义是:get 语法将对象属性绑定到查询该属性时将被调用的函数,有点抽象是不是?我们看文档中例子就行,代码如下:

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    return this.log[this.log.length - 1];
  },
};

console.log(obj.latest);
// Expected output: "c"

可以看到,在obj中使用了get functonName(){}的写法,那么这个作用是什么呢?答案是可以利用obj.functionName去返回动态计算值的属性,特别是不想以显示的方式返回的时候

当然,我们会可能会想,这种方式好像跟直接写函数差不太多,即下列代码所示:

const obj = {
  log: ['a', 'b', 'c'],
  latest() {
    return this.log[this.log.length - 1];
  },
};

console.log(obj.latest());
// Expected output: "c"

区别在于使用get在调用时无需加(),使其看起来像调用一个属性,这样更简洁和更具有语义性,且要执行的逻辑只是计算内部的属性值,这就是使用get去计算的意义

实现computed

computedVue响应式核心API,用于声明计算属性计算属性是依赖于其他的属性依赖而存在的,如果其他的属性依赖变化了,那么就会触发computed进行重新计算,进而得到最新的计算属性,我们可以看下书中的示例,代码如下:

const sumRes = computed(() => obj.foo + obj.bar);
  
effect(() => {
  // 在该副作用函数中读取 sumRes.value
  console.log(sumRes.value);
});

// 修改 obj.foo 的值
obj.foo++;

在这段示例代码中,sumRes就是声明的计算属性,其依赖于obj.fooobj.bar两个属性依赖,当obj.foo++执行后,会触发computed进而得到新的sumRes

需要注意的是,computed还具有缓存,即如果属于依赖不变的情况下,无论执行多少次,都不会触发computed的重新计算

那么computed是如何设计的呢?

其实读到这里,聪明的读者应该有个思路,就是sumRes就是getter所对应的obj,而sumRes.value就是触发了obj.getter,只不过这个getterget value(){},也就是:

const sumRes = {
    get value(){
        return ...
    }
}

初次实现computed

我们直接来看书中的源码是如何实现满足缓存的,代码如下:

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value;
  // dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算
  let dirty = true;
  
  const effectFn = effect(getter, {
    lazy: true
  });

  const obj = {
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn();
        // 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值
        dirty = false;
      }
      return value;
    }
  };

  return obj;
}

可以看到是添加了一个value变量存储getter计算过后的值,并设置了一个开关dirty,第一次为true即产生计算,而计算过后就会置为false,下一次读取时就不会重新计算

但现在又出现了一个问题,如果下次读取时obj.fooobj.bar发生了变化呢?在哪里将dirty置为true

别忘了我们还可以在effect中传入schedular,也就是在调度器中置为true,代码如下:

const effectFn = effect(getter, {  
  lazy: true,  
  // 添加调度器,在调度器中将 dirty 重置为 true  
  scheduler() {  
    dirty = true;  
  }  
});

执行的逻辑(简化)是怎么样的?

  1. 读取sumRes.value,触发getter
  2. getter执行时触发读取obj.fooobj.bar
  3. 此时activeEffect栈顶的是封装了getteteffectFn
  4. obj.fooobj.bareffectFn关联,执行完返回sumRes.value
  5. 当触发obj.foo改变时,取出封装了getteteffectFn的执行
  6. trigger中取出schedular执行,将ditry置为true

但现在,并不会重新输出sumRes.value的值

原因在于obj.fooobj.bar关联的effect是封装了getteteffectFn,也就是:

effect(() => obj.foo + obj.bar, {  
  lazy: true,  
  // 添加调度器,在调度器中将 dirty 重置为 true  
  scheduler() {  
    dirty = true;  
  }  
});

只会重新执行() => obj.foo + obj.bar,而不是console.log(sumRes.value),所以下一步是要解决如何修改完obj.foo后能够重新执行console.log(sumRes.value)的问题

完善computed

其实设计的关键在于schedular,我们知道在trigger中,如果存在schedular会执行schedular而不是effectFn,所以可以在schedular中执行console.log(sumRes.value),但真正执行的其实是执行封装其的effectFn

那该如何拿到这个console.log(sumRes.value)呢?可以设计一个关联,就是obj.value与封装console.log(sumRes.value)这个effectFn的关联

我们知道,在执行完获取sumRes.value之后,此时此时activeEffect栈顶的是封装了console.log(sumRes.value)effectFn,那么就可以在执行完获取sumRes.value之后调用track,代码如下:

const obj = {  
  get value() {
    if (dirty) {
      value = effectFn()
      dirty = false
    }
    // 当读取 value 时,手动调用 track 函数进行追踪
    track(obj, 'value')
    return value
  }
}

然后再在scheduler中,调用trigger,取出effectFn执行,代码如下:

// computed内
const effectFn = effect(getter, {
  lazy: true,
  scheduler() {
    dirty = true;
    trigger(obj, 'value')
  },
});

那么现在,当obj.foo++时,就会重新执行console.log(sumRes.value),也就实现了当obj.fooobj.bar变化时,会重新执行console.log(sumRes.value)这个effect的效果,具体的逻辑可看下图:

执行过程

这就是整个computed的实现原理

小记

这是写的第四篇关于vue3.js响应式设计的内容了,但发现看的人还是比较少的,不管是评论还是收藏数都几乎为
0,不知道是写的不好还是其他的原因

写的初衷还是如同开头说的那般,书中即使讲的明白,但如果缺少一定的基础,在逻辑上还是比较难梳理的,写出来一方面是方便我自己复习,另一方面也是希望能够帮助到想了解或者进阶学习Vue的同学

关于这篇,如果认真阅读了,最起码可以达到以下效果

  1. 学会如何去实现懒执行函数
  2. 了解和学习什么是getter
  3. 知道Vue团队是如何设计和实现computed
  4. 响应式系统在computed的实现逻辑
  5. 面试时问到上述也会答得出
  6. 其他…

谢谢大家的阅读,如有错误的地方请私信笔者

笔者会在近期整理后续章节的笔记发布至博客中,希望大家能多多关注前端小王hs

<think>嗯,用户想了解Vue.jscomputed属性的实现原理,特别是源码层面的分析。我之前已经整理过相关引用,现在需要结合这些资料来构造一个详细的回答。首先,我应该从Vue 2和Vue 3的不同实现入手,因为引用里提到了两者的源码解析和设计原理。 首先,用户的问题是关于computed实现原理,需要解释其响应式机制。根据引用[1],Vue 2中计算属性和侦听属性的区别在于计算属性是基于依赖的缓存,而引用[2]提到了Vue3computed的依赖追踪和副作用触发。同时,引用[3]的书籍可能提供了更系统的框架设计解析。 接下来,我需要理清Vue的响应式系统如何工作。Vue 2使用Object.defineProperty,而Vue 3改用Proxy,这会影响computed实现方式。计算属性本质是一个getter函数,依赖其他响应式数据,当依赖变化时才会重新计算。 然后,缓存机制是关键点。计算属性只有在相关依赖发生改变时才会重新计算,否则直接返回缓存值。这涉及到依赖收集和派发更新的过程,可能需要结合Watcher和Dep的概念来解释。在Vue 2中,每个计算属性对应一个Watcher,并且这个Watcher是惰性求值的,只有在被访问时才会计算。 另外,Vue 3中的computed通过effect和ReactiveEffect来实现,引用[2]中的代码示例展示了如何利用副作用函数来响应依赖变化。这里需要说明computed内部如何创建effect,以及如何通过调度器(scheduler)来控制重新计算的时机。 还要考虑源码结构,比如在Vue 2中,计算属性在初始化阶段被处理,定义在src/core/instance/state.js中的initComputed函数。而Vue 3computed API位于packages/reactivity/src/computed.ts,使用ComputedRefImpl类来管理依赖和值。 用户可能还想了解惰性求值和缓存的具体实现细节,比如脏检查机制(dirty标志位)。当依赖变化时,标记为dirty,下次访问时才会重新计算。这可以提升性能,避免不必要的计算。 此外,需要区分computed和watch的不同。计算属性是基于依赖的派生值,而watch是观察特定数据变化执行回调。从源码角度看,computed的Watcher和user Watcher可能有不同的配置选项,比如lazy和dirty的处理。 最后,总结实现原理时,要分点说明响应式依赖、缓存机制、惰性求值,并结合Vue 2和Vue3的不同实现方式。可能还需要提到源码中的关键函数和类,如Vue2的defineComputed、Watcher,Vue3ComputedRefImpl、track和trigger等。 确保回答结构清晰,逐步深入,并引用提供的资料来支持分析,比如引用[1][2][3]中的内容。同时,生成的相关问题要围绕实现细节、优化、与watch的区别以及应用场景展开,帮助用户进一步学习。</think>### Vue.js computed 属性实现原理分析 Vue 的 **计算属性(computed)** 核心在于**依赖追踪**和**惰性缓存**,其实现机制可分为以下关键环节: --- #### 一、响应式依赖绑定(以 Vue 3 为例) 1. **初始化阶段** ```javascript const sumRes = computed(() => obj.foo + obj.bar) ``` - 通过 `computed` 函数创建 `ComputedRefImpl` 对象[^2]。 - 内部通过 `ReactiveEffect` 包裹计算函数,建立依赖关系: ```typescript // 源码片段(reactivity/src/computed.ts) this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true // 标记为"脏数据",触发重新计算 trigger(this, "value" /* TriggerOpTypes.SET */) } }) ``` 2. **依赖收集** - 当首次访问 `sumRes.value` 时,触发计算函数的执行。 - 在计算过程中,访问的响应式变量(如 `obj.foo`)会通过 `track` 函数将当前 `ComputedRefImpl` 对象注册为依赖。 --- #### 二、缓存与惰性计算 1. **缓存机制** - 计算属性内部维护 `_value` 和 `_dirty` 标志位。 - 若依赖未变化(`_dirty` 为 `false`),直接返回缓存值 `_value`。 - 依赖变化时,标记 `_dirty` 为 `true`,但**不立即重新计算**,而是等待下次访问时触发。 2. **性能优化** ```javascript // 示例:多次访问同一个计算属性 console.log(sumRes.value) // 首次计算,标记_dirty=false console.log(sumRes.value) // 直接读取缓存 ``` - **避免重复计算**:依赖未变化时直接返回缓存值。 - **按需更新**:依赖变化后延迟到实际访问时再重新计算[^1]。 --- #### 三、与侦听属性(watch)的本质区别 | **特性** | **计算属性(computed)** | **侦听属性(watch)** | |----------------|------------------------------|-------------------------------| | **触发时机** | 依赖变化后**延迟计算** | 依赖变化后**立即执行回调** | | **返回值** | 必须返回一个值 | 无返回值,用于执行副作用操作 | | **设计目标** | 派生数据(缓存优化) | 响应数据变化(主动监听) | --- #### 四、Vue 2.x 实现差异 - **基于 Watcher 的惰性求值** Vue 2 中每个计算属性对应一个 `Watcher`,配置 `lazy: true` 实现惰性求值: ```javascript // 源码片段(src/core/instance/state.js) function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers[key] if (watcher.dirty) { watcher.evaluate() // 重新计算 } if (Dep.target) { watcher.depend() // 依赖收集 } return watcher.value } } ``` - **缓存通过 `dirty` 标志控制**,依赖更新时通过响应式系统的 `notify` 方法触发 `dirty` 标记更新。 --- ### 总结 计算属性的核心设计是**将函数式编程与响应式系统结合**,通过依赖追踪实现精准更新,利用缓存机制避免冗余计算。Vue 3 通过 `ReactiveEffect` 和调度器优化了性能,而 Vue 2 则依赖 `Watcher` 和 `Dep` 的经典实现[^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小王hs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值