Vue3 源码深度解析:Computed 的完整实现机制

本系列前三篇我们已经拆解了 reactivecollection handlerseffect 体系,本篇继续深入 Vue3 响应式系统的第四个核心:computed 派生值

本文依旧沿用统一结构:概念 → 架构 → 核心流程 → 源码逐段解析 → 与 effect 的关系 → 使用示例 → 扩展 → 潜在问题。


一、Computed 是什么?(概念篇)

在 Vue3 的响应式系统中,computed 属于 派生类型(Derived State)

  • 由其他响应式值计算得来
  • 自动追踪依赖
  • 自动缓存(不重复执行 getter)
  • 支持可写/只读两种模式
  • effect 解耦,不再使用 effect 实现

在新版实现中,computed 更像一个 "双重身份单元"

  • 作为 被动目标:被外部 effect 读取时,需要收集依赖(自身被依赖)
  • 作为 主动订阅者:自己要订阅其他响应式源,以便在源更新时"变脏"

这种结构使得 computed 更轻、更快、更可控。


二、Computed 的总体架构

在 Vue3 中,computed 的主要结构如下:

objectivec 复制代码
ComputedRefImpl
 ├─ _value           // 缓存的计算结果
 ├─ fn               // getter
 ├─ setter           // 可写 computed 才有
 ├─ dep              // 哪些 effect 依赖 computed
 ├─ deps / depsTail  // computed 依赖哪些 ref/reactive(反向链)
 ├─ flags            // 当前 computed 是否 dirty
 ├─ globalVersion    // 版本号控制是否要重新计算
 ├─ isSSR            // SSR 模式不同逻辑

其中最关键的是 flags(dirty) + globalVersion(版本校验)

这两个共同控制 computed 的缓存刷新逻辑。


三、核心流程:Computed 的运行机制

Vue3 中 computed 工作流程可以分为四个步骤:


1)初始化

  • 创建 ComputedRefImpl
  • 记录 getter
  • 若有 setter 则为可写模式
  • 标记为 dirty(需要首次计算)

2)首次访问 .value

  • 触发 dep.track() 收集依赖
  • 触发 refreshComputed(this)
  • 执行 getter 得到新值
  • 缓存 _value
  • 清除 dirty / 更新版本号

3)依赖更新 → 触发 notify()

如果 computed 所依赖的数据改变:

  • computed.flags |= DIRTY
  • 下一次访问 .value 时会重新执行 getter

4)再次访问 .value

若 dirty = false 且版本号未变:

不重新计算,直接返回缓存

若 dirty = true 或版本号变化:

重新计算


四、源码逐段解析(与你前三篇风格完全一致)

以下代码全部来自你给的源码,我将按模块拆解并解释。


① 依赖导入

javascript 复制代码
import { isFunction } from '@vue/shared'
import {
  DebuggerEvent,
  DebuggerOptions,
  EffectFlags,
  Subscriber,
  activeSub,
  batch,
  refreshComputed,
} from './effect'
import { warn } from './warning'
import { Dep, Link, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'

解析

  • batch() → 批量通知 effect,避免频繁触发
  • refreshComputed() → computed 的核心刷新逻辑
  • Dep → 用于记录计算属性被哪些 effect 依赖
  • globalVersion → 全局版本号(优化)

computed 不再依赖 effect,而是依赖 Dep(更轻量)。


② ComputedRefImpl 类定义

typescript 复制代码
export class ComputedRefImpl<T = any> implements Subscriber {

computed 自身是一个 Subscriber(订阅者)

它能订阅其他 reactive/ref。


③ 内部字段

ini 复制代码
_value: any = undefined
dep: Dep = new Dep(this)
__v_isRef = true
__v_isReadonly: boolean
deps?: Link = undefined
depsTail?: Link = undefined
flags: EffectFlags = EffectFlags.DIRTY
globalVersion: number = globalVersion - 1
isSSR: boolean

字段解析

字段 作用
_value 缓存的计算结果
dep 谁依赖了这个 computed
deps computed 依赖的响应式源(反向)
flags 是否 dirty,需要重新计算
globalVersion 避免重复计算的版本号控制

④ 构造函数

kotlin 复制代码
constructor(fn, setter, isSSR) {
  this[ReactiveFlags.IS_READONLY] = !setter
  this.isSSR = isSSR
}

没有 setter → readonly computed。

这部分设计和前三篇响应式系统一致。


⑤ notify:依赖发生变化时触发 dirty

kotlin 复制代码
notify() {
  this.flags |= EffectFlags.DIRTY

  if (
    !(this.flags & EffectFlags.NOTIFIED) &&
    activeSub !== this
  ) {
    batch(this, true)
    return true
  }
}

原理说明

当依赖更新时:

  • 标记本 computed 为 dirty
  • 通过 batch 通知依赖它的 effect
  • 避免 self-recursion(避免自己递归触发自己)

这与上一篇 Collection 里的触发机制一致。


⑥ get value:读取并刷新缓存

kotlin 复制代码
get value() {
  const link = this.dep.track()

  refreshComputed(this)

  if (link) {
    link.version = this.dep.version
  }
  return this._value
}

核心关键点(非常重要)

  1. dep.track()
    表示 "外部 effect 正在依赖我"
  2. refreshComputed(this)
    若 dirty 或版本号变化 → 执行 getter
  3. 返回缓存值

这是 computed 靠缓存优化性能的关键。


⑦ set value:支持可写 computed

kotlin 复制代码
set value(newValue) {
  if (this.setter) {
    this.setter(newValue)
  } else {
    warn('Write operation failed: computed value is readonly')
  }
}

这个很好理解。


⑧ computed() 工厂函数

ini 复制代码
export function computed(getterOrOptions, debugOptions, isSSR = false) {
  let getter
  let setter

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.onTrack = debugOptions.onTrack
    cRef.onTrigger = debugOptions.onTrigger
  }

  return cRef
}

支持两种形式

  • computed(getter)
  • computed({ get, set })

同 Vue3 文档。


五、Computed 与 Effect 的关系

Vue3 中:

项目 Vue2 Vue3
computed 内部实现 基于 watcher 不再使用 effect
依赖收集 watcher.deps dep + version
缓存策略 lazy watcher version + dirty

Computed 更轻、更纯,不会参与 effect 的副作用队列。


六、实战示例

scss 复制代码
const count = ref(1)

const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2
count.value++
console.log(plusOne.value) // 3(重新计算)

使用体验不变,但实现更加高效。


七、扩展:Computed 为什么要分版本号?

原因:提升性能、减少不必要计算

例如:

多个 ref 改变但 getter 内只依赖其中一个时,版本号能避免无效计算。

Vue3 响应式系统中 version 机制广泛存在:

  • ref
  • reactive
  • computed

都依赖 version。


八、潜在问题与注意事项

  1. getter 必须是纯函数(不要做副作用)
  2. computed 不适合包含异步(会变成 Promise)
  3. 多层嵌套 computed 可能产生链式 dirty --- Vue 已做优化
  4. setter 写入不要和 getter 逻辑矛盾

总结

本篇从 computed 的整体设计、数据结构、dirty 机制、版本号策略、依赖链结构,到源码逐段拆解,完整讲解了 Vue3 计算属性的底层实现,使其风格与前三篇完全保持一致。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
消失的旧时光-19438 小时前
Kotlinx.serialization 对多态对象(sealed class )支持更好用
java·服务器·前端
少卿8 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技8 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
广州华水科技8 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮8 小时前
umi4暗黑模式设置
前端
8***B8 小时前
前端路由权限控制,动态路由生成
前端
军军3609 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1239 小时前
Vue基础知识(一)
前端·javascript·vue.js
terminal0079 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
我的小月月9 小时前
🔥 手把手教你实现前端邮件预览功能
前端·vue.js