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 辅助生成,并由作者整理审核。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax