Vue 3 设计与实现

第 1 章 为什么在 2026 年重新理解 Vue

作者 杨艺韬 · 9,664 字

第 1 章 为什么在 2026 年重新理解 Vue

本章要点

  • Vue 的三次范式蜕变:Options API → Composition API → Vapor Mode
  • Vapor Mode 如何绕过虚拟 DOM,直接生成命令式 DOM 操作代码
  • Alien Signals 为何用版本计数取代 Set-based 依赖追踪
  • 本书与其他 Vue 源码书的本质区别
  • Vue 3.6 monorepo 的全景架构图

你可能会问:市面上已经有那么多 Vue 源码解析的书了,为什么还要再写一本?

这个问题的答案,藏在 Vue 过去十年的三次蜕变里。

2026 年这个时间点的特殊性

如果把这本书放在 2020 年写,故事会简单得多:讲 Options API 到 Composition API 的演进、讲 Proxy 替代 defineProperty、讲 PatchFlags 优化 diff——这些都是 Vue 3 早期的标志性技术,也是市面上绝大多数 Vue 源码书覆盖的内容。

但写在 2026 年的这本书,注定要回答一个更大的问题:Vue 作为一个前端框架,已经从"怎么让用户写得更舒服"的关注,跨进了"怎么让框架跑得更快"的深水区。Vapor Mode、Alien Signals 这些 3.5 / 3.6 的新机制,代表着 Vue 团队一次集体的价值重排——"用户写法的便利是已经解决的问题,现在要解决的是框架自己怎么突破天花板"。

这一转向对读者的意义非常深远:读懂 Vue 3.6 的本质,就是读懂"一个成熟框架下一步往哪里走"的集体共识。Svelte、Solid、Qwik 这些"编译期优化派"在 2020 年前后被 Vue / React 团队当作实验性探索、如今这些探索的共识已经反过来塑造了 Vue / React 本身。你如果还停留在 Vue 3.0-3.4 的知识版本,实际上错过的不只是几个 API——是前端框架集体演进的一次关键方向调整。

本章不讲源码细节,只回答一个问题:这本书打开的视角,为什么值得你花几十个小时沉浸进去。读完这一章,你心里会有一个整体地图——后面 18 章每一章讲什么、它和其他章节的关系、为什么要这个顺序。有了这张地图,再进入具体章节你会知道"我在探索哪一块"——不会像在迷雾里摸索。

1.1 Vue 的三次蜕变:Options → Composition → Vapor

这三次蜕变不是独立事件——它们有一条共同的线索:每一次都对应着"框架核心价值主张"的重新调整。Options 时代的核心价值是"让初学者能在半小时内写出第一个应用";Composition 时代是"让大型项目的逻辑复用有出路";Vapor 时代是"让一个写了十年的成熟框架再向前突破一个数量级的性能"。看蜕变不要只看 API 变化,要看驱动变化的价值重排——这种视角会让你读后面任何一个框架的 RFC、版本说明、release note 时都更快抓住要点。

第一次蜕变:Options API(2014–2019)

2014 年,当尤雨溪发布 Vue 0.x 时,前端世界正被 Angular 1 的复杂概念搞得晕头转向——Scope、Directive、Digest Cycle、依赖注入——一个简单的待办清单应用需要理解整套概念体系才能写出来。

Vue 的回应是激进的简洁。它提出了一个大胆的设想:如果一个组件的全部逻辑,都可以用一个普通的 JavaScript 对象来描述呢?

// Vue 2 Options API — 一个对象描述一切
export default {
  data() {
    return {
      count: 0,
      doubleCount: 0
    }
  },
  computed: {
    tripleCount() {
      return this.count * 3
    }
  },
  watch: {
    count(newVal: number) {
      this.doubleCount = newVal * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

这段代码的魔力在于它的声明式组织:数据放 data,计算属性放 computed,方法放 methods,侦听器放 watch。新手不需要理解任何框架概念,只需要知道"把东西放在正确的格子里"。

🔥 深度洞察

Options API 的设计哲学是按类型组织(organize by type):所有的状态放一起,所有的方法放一起,所有的计算属性放一起。这在小型组件中非常直观。但它隐含了一个根本性的架构假设——一个组件只做一件事。当组件承担多个关注点时,同一个功能的状态、计算和方法被强制拆散到不同的选项中,代码的物理组织与逻辑组织出现了系统性的错位。

Options API 的驱动力是降低门槛。它成功了——Vue 在 2016-2019 年间成为增长最快的前端框架,很大程度上归功于这种"一个对象搞定一切"的简洁性。

但随着应用规模的增长,Options API 的裂缝开始显现。

让我把这种"裂缝"讲具体一些。想象你在做一个用户中心页面,需要展示用户信息、让用户改昵称、让用户上传头像、让用户查看历史阅读记录。这四件事在用户视角是一个功能——都围绕"用户资料"这个业务对象。但在 Options API 的组织里,它们会被拆散到四个地方:

  • data 里有 profile(信息)、newNickname(表单)、avatarFile(上传文件)、history(历史)
  • computed 里有派生出来的 displayNameisValidNicknameavatarPreviewgroupedHistory
  • methods 里有 updateProfilechangeNicknameuploadAvatarfetchHistory
  • watch 里有 nickname 校验的响应、avatar 预览的更新……

四个功能被切成了四刀、分散在四个 section。当你三个月后回来修"改昵称"的逻辑时,你需要在这四个 section 之间来回跳——要改 data 里的 newNickname 类型、改 computed 里的 isValidNickname 校验、改 methods 里的 changeNickname 流程、改 watch 里的相应副作用。本来是一个功能的修改,要你心里装四处代码——这就是"按类型组织"的深层代价。这不是小问题——在上千组件的大型项目里,这种"改一处牵四处"的代价会把团队效率拖到地上。

第二次蜕变:Composition API(2020–2024)

2019 年夏天,Vue 团队发布了 Composition API 的 RFC 提案。社区反应非常激烈——支持者认为这是 Vue 成为"企业级可维护"框架必走的一步,反对者则担心 Vue 失去"简单"这个最大的卖点。RFC 讨论贴下面长达上万条评论,前后持续了数月。最终 Vue 团队选择了"共存而非替代"的路线——Composition API 作为可选方案与 Options API 并存,由开发者自行选择。这是一次典型的"社区治理 + 技术判断"共同塑造结果的案例——理解了它,你就明白 Vue 为什么在开源社区里一直被认为是"决策节制"的代表。

2020 年,Vue 3 带来了 Composition API。它不是对 Options API 的增量改进,而是对组件逻辑组织方式的根本重构。

驱动力是什么?逻辑复用。

在 Options API 中,复用逻辑的方式是 Mixins。但 Mixins 有三个致命问题:命名冲突、隐式依赖、数据来源不透明。当一个组件混入三四个 Mixin 后,this.xxx 到底来自哪里,只有上帝知道。

Composition API 的回答是:用函数替代选项,用显式的返回值替代隐式的 this 挂载。

// Vue 3 Composition API — 函数组合一切
import { ref, computed, watch } from 'vue'

function useCounter() {
  const count = ref(0)
  const doubleCount = ref(0)
  const tripleCount = computed(() => count.value * 3)

  watch(count, (newVal) => {
    doubleCount.value = newVal * 2
  })

  function increment() {
    count.value++
  }

  return { count, doubleCount, tripleCount, increment }
}

// 在组件中使用
export default {
  setup() {
    const { count, doubleCount, tripleCount, increment } = useCounter()
    // 可以同时组合多个逻辑单元
    const { user, login } = useAuth()
    const { items, fetchItems } = useInventory()

    return { count, doubleCount, tripleCount, increment, user, login, items, fetchItems }
  }
}

注意关键的变化:

  1. 按功能组织(organize by feature)取代了按类型组织——useCounter 把计数相关的状态、计算和方法放在一起
  2. 显式依赖——每个 composable 的输入和输出一目了然,不存在隐式的 this
  3. TypeScript 友好——函数的参数和返回值天然支持类型推断,不需要 Vue.extend() 等类型体操

🔥 深度洞察

从 Options API 到 Composition API 的转变,本质上是编程范式的一次经典迁移:从面向对象(一个组件是一个对象,逻辑通过继承和混入复用)到函数式组合(一个组件是多个函数的组合,逻辑通过函数的输入输出复用)。这一转变映射了更广泛的软件工程共识——组合优于继承。React Hooks 在 2018 年走了同一条路,Svelte 5 的 Runes 在 2024 年也做了类似的选择。这不是偶然的趋同,而是前端框架集体发现了同一个真理。

第三次蜕变:Vapor Mode(2025–)

如果说前两次蜕变是"怎么让用户写得更好",第三次蜕变的动机完全不同——是"用户已经写得够好了,现在要让框架自己跑得更快"。这个转向发生在 Vue 成为"成熟框架"之后,也就意味着 Vue 从"**追赶"阶段进入了"突破天花板"阶段。这是一个质变,也是 Vue 3.6 能成为这本书主角的根本原因。

如果说 Options → Composition 是逻辑组织的范式转换,那么 Vapor Mode 是渲染模型的范式转换。

2025 年,Vue 团队正式推出 Vapor Mode。这一次,被重构的不再是开发者编写代码的方式,而是框架将模板转化为 DOM 操作的方式。

驱动力是什么?性能天花板。

虚拟 DOM 的核心理念是:用 JavaScript 对象描述 UI 结构(VNode 树),当数据变化时,生成新的 VNode 树,与旧树进行 diff,找出差异,最后将差异应用到真实 DOM。

这套机制有一个根本性的问题:无论模板多么简单,每次更新都必须走完"创建新树 → diff → patch"的完整链路。 即使一个组件的模板中只有一个动态绑定,VNode diff 依然会遍历整棵子树。

Vue 3.0 通过 Block Tree 和 PatchFlags 做了大量编译期优化来缓解这个问题——但优化的本质是"让 diff 更快",而不是"不做 diff"。

Vapor Mode 的回答更加彻底:如果编译器已经知道哪些是动态的,为什么还需要 diff?

// 一个简单的模板
// <template>
//   <div class="container">
//     <h1>{ { title } }</h1>
//     <p>{ { description } }</p>
//   </div>
// </template>

在传统 VDOM 模式下,编译输出大致是:

// VDOM 编译输出 — 每次更新都生成完整 VNode 树
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from 'vue'

function render(_ctx: any) {
  return (_openBlock(), _createElementBlock("div", { class: "container" }, [
    _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
    _createElementVNode("p", null, _toDisplayString(_ctx.description), 1 /* TEXT */)
  ]))
}

每次 titledescription 变化,都要:

  1. 调用 render() 生成新的 VNode 树
  2. 与旧 VNode 树 diff
  3. 找到 PatchFlag 标记的动态节点
  4. 执行 patchElement 更新真实 DOM

而在 Vapor 模式下,编译输出截然不同:

// Vapor 编译输出 — 直接操作 DOM,无 VNode
import { setText, template, effect } from 'vue/vapor'

const _tmpl = template('<div class="container"><h1></h1><p></p></div>')

function render(_ctx: any) {
  const root = _tmpl()                      // ← 一次性创建 DOM 结构
  const h1 = root.firstChild!               // ← 直接获取 DOM 引用
  const p = h1.nextSibling!

  effect(() => setText(h1, _ctx.title))      // ← 精确绑定:title 变 → 只更新 h1
  effect(() => setText(p, _ctx.description)) // ← 精确绑定:description 变 → 只更新 p

  return root
}

注意根本性的区别:

  1. 没有 VNode——template() 直接返回真实 DOM 节点
  2. 没有 diff——没有新旧树比较,每个动态绑定通过独立的 effect 直接更新对应的 DOM 节点
  3. 精确更新——title 变化只触发 h1 的更新,descriptioneffect 完全不运行

这就像从"每次导航都重新渲染整张地图"变成了"GPS 只更新你的位置标记"。

为什么"不做 diff"的想法迟到了十年?

虚拟 DOM 是 React 在 2013 年推出的明星设计。它成功解决了"声明式 UI 更新"这个大问题——开发者只需要描述"页面应该长什么样",不再关心"怎么从旧状态更新到新状态"。这种"把状态变化的计算交给框架"的哲学影响了整整一代前端工程师,也让 React 在 2014-2018 年间取得压倒性胜利。Vue 2、Angular 2、Ember 后来都向这个模型靠拢——虚拟 DOM 一度被视为前端框架的"标准底座"。

但虚拟 DOM 的美好里藏着一个昂贵的前提:运行时必须每次都生成一棵完整的 VNode 树,即使 99% 的节点在两次渲染之间毫无变化。这个"生成+比较"的过程在组件简单时可以忽略,在复杂组件 + 频繁更新的场景下就会变成性能瓶颈。React 团队很早就意识到了这点——React.memouseMemouseCallback、Concurrent Rendering 都是在修修补补;但修补永远到不了"不做 diff"的彻底解法。

真正打破"必须有虚拟 DOM"成见的是三个人/项目:Rich Harris 的 Svelte(2019 年 Svelte 3 起)、Ryan Carniato 的 Solid.js(2021 年 1.0)、Miško Hevery 的 Qwik(2022)。他们用不同的路径证明了"细粒度响应式 + 编译期分析就能取代 VDOM"——而且更快、更省内存。这些方案早期被主流框架当作"小众选手",但它们的性能基准数据太漂亮了——js-framework-benchmark 的榜单前列几乎被 Solid / Svelte 包揽。

到 2024-2025 年,Vue 和 React 团队都不得不正视这个方向。Vue 选择的路径是"共存"——保留 VDOM 模式给需要兼容老代码的场景,同时提供 Vapor Mode 给性能关键的新场景。这是一个非常 Vue 风格的决定:不做激进的破坏性切换,让用户按自己的节奏采用新范式。这种"渐进"是 Vue 能持续保持大批用户忠诚度的关键原因——你不会被迫"全部重写",可以选择关键场景先切。

性能基准

Vue 团队公布的基准测试数据(js-framework-benchmark):

场景 VDOM 模式 Vapor 模式 提升
创建 1000 行 基准 ~30% 更快 显著
更新部分行 基准 ~50% 更快 非常显著
选择行(高亮切换) 基准 ~70% 更快 质变
交换行 基准 ~40% 更快 显著
内存占用 基准 ~30% 更低 显著
启动时间(TTI) 基准 ~20% 更快 可观

🔥 深度洞察

Vapor Mode 的意义超越了 Vue 自身。它标志着前端框架竞争的重心,从"运行时性能优化"正式转向"编译期信息利用"。Svelte 从一开始就走这条路,Solid.js 用细粒度响应式实现了同样的效果,Angular 也在向 Signals 转型。Vue 的 Vapor Mode 代表着一个行业共识的形成:虚拟 DOM 不是终极方案,编译器知道的信息应该被充分利用。 虚拟 DOM 在 2013 年由 React 引入时是一个革命性的想法——用声明式的方式描述 UI,让框架处理更新。但十年后,我们发现"让框架处理更新"不一定需要"构建虚拟树再做 diff"。编译器可以在编译期就确定最优的更新策略。

1.2 Vue 3.6 的破局:Vapor Mode 无虚拟 DOM

上一节从"三次蜕变"的叙事线带你快速走过了 Vue 的技术史。本节把焦点聚在最新的那次蜕变——Vapor Mode——用更深一层的视角讨论它的架构意义。这不是为了重复,而是因为 Vapor Mode 对 Vue 未来 3-5 年的形态有深远影响,值得单独拿出来剖析。

graph LR
    subgraph Traditional["传统 VDOM 模式"]
        T1["Template"] --> T2["Compiler\n(翻译官)"]
        T2 --> T3["render()\nVNode 树"]
        T3 --> T4["Runtime\nDiff + Patch"]
        T4 --> T5["DOM"]
    end
    subgraph Vapor["Vapor Mode"]
        V1["Template"] --> V2["Compiler\n(施工队长)"]
        V2 --> V3["直接 DOM\n操作代码"]
        V3 --> V5["DOM"]
    end

    style Traditional fill:#fef3c7,stroke:#f59e0b
    style Vapor fill:#dcfce7,stroke:#22c55e

上一节我们从宏观角度看了 Vapor Mode 的编译输出差异。这一节让我们深入一些,理解 Vapor Mode 在架构层面意味着什么。

编译器的角色转变

在传统 VDOM 模式中,编译器的角色是"翻译官"——将模板翻译为 render 函数,该函数返回 VNode 树的描述。运行时拿到这棵 VNode 树后,自己负责 diff 和 patch。

在 Vapor 模式中,编译器的角色升级为"施工队长"——它不仅知道要建什么(模板结构),还直接生成施工指令(DOM 操作代码)。运行时不再需要"看图纸做比较",只需要"按指令执行"。

┌─────────────────────────────────────────────────────────────┐
│                    传统 VDOM 模式                             │
│                                                              │
│  Template → Compiler → render() → VNode Tree                │
│                                      ↓                       │
│                              Runtime: diff + patch → DOM     │
│                                                              │
│  编译器:只负责翻译                                            │
│  运行时:负责 diff + patch(重活)                              │
├─────────────────────────────────────────────────────────────┤
│                    Vapor 模式                                 │
│                                                              │
│  Template → Compiler → DOM 操作指令 + effect 绑定             │
│                              ↓                               │
│                        Runtime: 执行 effect → DOM             │
│                                                              │
│  编译器:负责翻译 + 优化 + 生成精确更新代码(重活)               │
│  运行时:只执行 effect(轻活)                                  │
└─────────────────────────────────────────────────────────────┘

这种角色转变有一个深刻的含义:复杂度从运行时转移到了编译期。 用户不感知编译器的复杂度,但每一毫秒的运行时开销都会被用户感知到。因此,这是一次对用户体验的净优化。

"运行时成本 = 每个用户的每次访问都承担编译期成本 = 每次构建时承担一次"——这个成本结构的差异在大规模 Web 应用里的放大效应非常可观。一个被 10 万用户每天访问的页面,如果在运行时多花 10ms,意味着每天多耗费 10 万 × 10ms = 1000 秒的真实用户时间。如果这 10ms 能在编译期提前做掉,每天的 10 万次访问就省下了 1000 秒。编译期多花 1 秒 vs 运行时每天省 1000 秒——这个比例在生产环境里是一笔非常划算的账。把成本推向它最便宜的地方——这是整个"编译期优化"思潮的共同哲学,Vapor Mode 只是这个哲学在 Vue 这里的具体落地。

Vapor 的渐进式采用

Vue 3.6 的一个精妙设计是:Vapor Mode 不是一个全有或全无的选择。你可以在同一个应用中混用 VDOM 组件和 Vapor 组件:

// 传统 VDOM 组件
import LegacyComponent from './LegacyComponent.vue'

// Vapor 组件(通过 .vapor.vue 后缀或编译选项标识)
import FastComponent from './FastComponent.vapor.vue'

// 在同一个应用中混用
export default {
  components: { LegacyComponent, FastComponent }
}

这意味着你可以渐进式地迁移——性能关键的组件先切换到 Vapor,其余组件保持不变。这与 Vue 一贯的"渐进式框架"哲学一脉相承。

"渐进式"到底意味着什么?——在框架设计里,这个词几乎被滥用到成了营销口号。Vue 的"渐进式"有其具体含义:任何新特性的引入,不强制已有代码做任何修改。Composition API 进来时没有让 Options API 消亡,.vue 文件多了 <script setup> 但老的 Options 写法依然跑;TypeScript 支持加强了但 JavaScript 写法依然一行不用改;Pinia 取代 Vuex 时,Vuex 4 和 Pinia 可以在同一个应用里共存,分模块逐步迁移。

Vapor Mode 把这个哲学推到了最极致——不仅允许"Options / Composition 共存",甚至允许"VDOM 组件和 Vapor 组件在同一个渲染树里混跑"。技术上这比前几次"共存"难很多,因为底层的运行时不同(VDOM 跑 patch / Vapor 跑 effect)、生命周期的触发时机不同、slot 的传递机制不同。Vue 团队硬是通过"兼容层"让两个运行时在组件边界上无缝对话——代价是核心团队多花了一整年时间做兼容性工程,但这件事做完之后,任何 Vue 3 的老项目都能"先把性能关键的几个组件切到 Vapor、其他维持不变",享受到 Vapor 的收益而无需面对重写成本。这是一次真正意义上的"升级红利给所有人"——不是只给新项目。

1.3 Alien Signals:响应式系统的第三次重写

Vapor Mode 改写了 Vue 的"视觉层怎么更新"——Alien Signals 改写的是 Vue 的"数据怎么感知变化"。两者都是 Vue 3.6 的重大技术升级,但解决的是完全不同维度的问题:Vapor 关心"DOM 怎么响应 state 变化更新",Alien Signals 关心"state 变化怎么精确、高效地通知依赖者"。理解这两者是两件事、不是一件事,是你读透本书后续章节的第一步——本书第 4、5 章会详述 Alien Signals 的实现,第 10 章会详述 Vapor Mode,但它们的设计哲学都可以追到本节。

如果 Vapor Mode 是渲染层的革命,那么 Alien Signals 就是数据层的革命。

为什么需要第三次重写

让我们回顾 Vue 响应式系统的演进:

第一代:Vue 2 — Object.defineProperty

// Vue 2 的响应式原理(简化)
function defineReactive(obj: any, key: string, val: any) {
  const dep = new Dep()  // 每个属性一个依赖收集器

  Object.defineProperty(obj, key, {
    get() {
      dep.depend()       // 收集当前正在执行的 Watcher
      return val
    },
    set(newVal: any) {
      val = newVal
      dep.notify()       // 通知所有 Watcher 更新
    }
  })
}

局限:无法检测属性的添加/删除,无法拦截数组索引赋值,需要 Vue.set() 等补丁 API。

第二代:Vue 3.0–3.4 — Proxy + Set-based tracking

// Vue 3.0 的响应式原理(简化)
// packages/reactivity/src/effect.ts

let activeEffect: ReactiveEffect | undefined

class ReactiveEffect {
  deps: Set<ReactiveEffect>[] = []  // ← 每个 effect 维护它的依赖集合

  run() {
    activeEffect = this
    const result = this.fn()
    activeEffect = undefined
    return result
  }
}

function track(target: object, key: string | symbol) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)       // ← WeakMap<target, Map<key, Set>>
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = depsMap.get(key)
  if (!dep) depsMap.set(key, (dep = new Set()))
  dep.add(activeEffect)                     // ← Set.add()
  activeEffect.deps.push(dep)               // ← 反向记录,用于 cleanup
}

function trigger(target: object, key: string | symbol) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect.run())      // ← Set.forEach() 遍历通知
  }
}

这套系统解决了 Vue 2 的所有局限,但引入了新的开销:

  1. 内存:每个响应式属性对应一个 Set,每次 effect 求值都要 cleanup(清除旧依赖)再重建
  2. CPUSet.add()Set.delete()Set.forEach() 虽然单次不慢,但在大规模依赖图中累积起来不可忽视
  3. GC 压力:频繁的 Set 创建和销毁产生大量短生命周期对象

第三代:Vue 3.5–3.6 — 版本计数 + 双向链表(Alien Signals)

// Vue 3.6 Alien Signals 的响应式原理(简化)
// packages/reactivity/src/effect.ts

interface Signal {
  _version: number         // ← 全局版本号,每次变化递增
}

interface Computed {
  _version: number         // ← 自身版本号
  _globalVersion: number   // ← 上次求值时的全局版本号
  _value: any
  _fn: () => any
}

// 当 signal 的值变化时
function signalSet(signal: Signal, value: any) {
  signal._value = value
  signal._version++         // ← 只增加版本号,不遍历通知任何人
  globalVersion++            // ← 全局版本号递增
}

// 当 computed 被读取时
function computedGet(computed: Computed): any {
  if (computed._globalVersion !== globalVersion) {
    // 全局有变化发生,检查自身依赖是否真的变了
    if (checkDirty(computed)) {
      computed._value = computed._fn()    // ← 惰性求值:只在被读取时才重算
      computed._version++
    }
    computed._globalVersion = globalVersion
  }
  return computed._value
}

关键区别在于:

  1. 没有 Set——依赖关系通过双向链表维护,不需要频繁创建/销毁集合
  2. 没有主动通知——signal 变化时只递增版本号,不遍历依赖者
  3. 惰性求值——computed 只在被读取时才检查是否需要重算
  4. O(1) 脏检查——版本号比较是整数操作,极快

为什么 Set-based → Version Counting

从""到""这个转变在计算机科学里不是新鲜事——操作系统的 IO 模型(select / poll / epoll vs 同步阻塞)、数据库的事务日志(WAL vs immediate commit)、分布式系统的事件分发(push vs pull)都经历过类似的设计迁移。前端响应式系统只是这场旷日持久的"推拉之争"在 Web 领域的最新章节——能借鉴的前人经验非常多。

用一个更生活化的类比:

Set-based 模型(旧) 像一个快递员,每次有包裹(数据变化)就立刻挨家挨户送到所有订阅者门口,不管他们在不在家(是否需要这个值)。

Version counting 模型(新) 像一个公告栏。快递站(signal)只在公告栏上更新版本号。住户(computed/effect)只有在出门(被读取/执行)时才看一眼公告栏——如果版本号变了,才去取包裹。

Set-based(推模型):
  signal 变化 → 遍历 Set → 通知 effect1, effect2, effect3 → 全部重算
  (即使 effect2 和 effect3 的结果在本轮 tick 中没人读取)

Version counting(拉模型):
  signal 变化 → version++ (完毕,0 开销)
  ...
  读取 computed → 检查 version → 需要重算 → 重算 → 返回新值
  (只有真正被读取的 computed 才会重算)

🔥 深度洞察

从推模型到拉模型的转变,反映了一个深刻的工程哲学:延迟决策比提前决策更高效。 在推模型中,系统在数据变化的瞬间就做出"通知所有人"的决策——但此时它并不知道哪些人真的需要新值。在拉模型中,系统将"是否需要新值"的决策延迟到真正需要的时刻——此时信息是完备的,可以做出最优决策。这一原则在软件工程中反复出现:懒加载、写时复制(Copy-on-Write)、惰性求值(Lazy Evaluation)——它们的共同本质都是将工作推迟到信息最充分的时刻

量化提升

指标 Vue 3.4(Set-based) Vue 3.6(Alien Signals) 原因
内存 / 响应式对象 高(Set + WeakMap 开销) 低(~40%,链表 + 整数) 消除 Set 分配
依赖追踪 O(n) cleanup + 重建 O(1) 版本比较 无需重建依赖集合
computed 触发率 依赖变 → 立刻重算 依赖变 → 读取时才重算 惰性求值消除无用计算
GC 暂停 频繁(短生命周期 Set) 极少(几乎无临时分配) 结构性消除 GC 压力
大规模 Signal 图 性能随依赖数线性下降 性能几乎不受依赖数影响 O(1) vs O(n)

Alien Signals 背后的人

Alien Signals 不是 Vue 团队原创——它最初是一个独立开源项目,作者 Johnson Chu 花了大约两年时间反复实验"前端响应式系统的最小化实现"。他在技术博客上公开的测试数据打动了 Vue 核心团队(Evan You 亲自跟进了相当一段时间的讨论),最终在 2025 年决定把 Alien Signals 的算法采纳进 Vue 3.5+ 的响应式系统。

这件事有两个值得记住的点。第一,开源社区的力量——Vue 这样一个拥有亿级用户的框架,响应式底层还愿意采纳一个外部开发者的创新方案,说明 Vue 团队的技术判断优先于"non-invented-here"自尊,这在大型开源项目里并不常见。第二,优秀的底层方案值得被重复发现——Johnson Chu 的 Alien Signals 本质上是把"惰性求值 + 版本号 + 双向链表"这几个计算机科学里存在几十年的老东西组合得非常精巧,并没有发明新的算法原语。创新不一定要发明新东西,把老东西用在正确的地方、调到最优的形态就够了。这是写出"优雅"而不是"复杂"代码的共同路径。

1.4 本书与其他 Vue 书的区别

市面上 Vue 相关的技术书籍,大致可以分为三类。理解它们的定位差异,有助于你判断本书是否适合你:

维度 API 实战书 早期源码书(3.0–3.4) 本书(3.6)
目标 教你用 Vue 写应用 教你理解 Vue 3.0 的内部实现 教你理解 Vue 当前的设计及演进动力
响应式 ref/reactive 怎么用 Set-based tracking 源码 Alien Signals 版本计数源码
渲染 组件、插槽、指令用法 VNode + patch + diff 源码 Vapor Mode 编译输出 + 对比 VDOM
编译器 很少涉及 基础编译流程 Vapor 编译器全链路
横向对比 偶尔提及 React 系统性对比 React/Svelte/Solid
版本 Vue 3.0–3.4 Vue 3.0–3.4 Vue 3.6.x
适合 Vue 入门/进阶开发者 想了解旧版实现的开发者 想理解当前设计的高级开发者

在技术领域,理解"为什么旧方案被替换"往往比理解"新方案怎么工作"更有教育意义。本书两者兼顾——你会看到 Set-based tracking 的局限如何催生了 version counting,VNode diff 的天花板如何催生了 Vapor Mode。

这一节的定位——帮你做决定

技术书市场良莠不齐。有些"Vue 源码书"你翻十页就发现它只是把官方文档重新抄了一遍;有些书极其深入但版本停留在 Vue 3.0,学完发现讲的东西已经被 Vue 3.6 整个替换。买书前你应该先问一句"这本书能给我什么别处拿不到的东西"——如果答案含糊,就不值得投入几十个小时。

本书能给你的独特价值有三条:

  1. 当前版本的权威源码讲解——2026 年 Vue 3.6 发布后最新的响应式、编译器、运行时变化都在本书里,不是引用三年前的旧源码。
  2. 不只讲"是什么",更讲"为什么"——每一个设计决策我们都会追到"替代方案是什么、为什么 Vue 没选、代价是什么"这一层,让你理解 Vue 之所以是今天这样,是一次次 trade-off 积累的结果。
  3. 跨框架视角——React、Svelte、Solid、Qwik 的同类特性会被系统性地对比,让你不只是"Vue 的熟练工",而是"现代前端框架的通读者"。

后面每一章都会在开头说明"本章和前后章节的关系、目标读者完成本章后能做什么"——这不是我强迫自己写的模板,是我认为技术书作者欠读者的一项基本责任。

1.5 Vue 源码全景图

读懂一个框架的源码前,你需要先建立"这个框架由哪些部分组成"的全局视角。直接跳进某一个包的源码里很容易迷路——你在读 @vue/runtime-core 里的 mountComponent 时需要知道它和 @vue/reactivity 里的 effect 是什么关系,需要知道它的参数是从哪个 @vue/compiler-dom 生成的代码传过来的。没有全景图,你每读一个函数都会被跨包引用打断思路。这一节就是为你搭好这张地图——后续 18 章每讲到一个文件路径,你都能立刻定位到它在全景图里的哪一块。

在深入各个模块之前,让我们先建立一幅 Vue 3.6 源码的全景地图。

Monorepo 结构

Vue 3 采用 monorepo 架构,核心仓库 vuejs/core 包含 20+ 个包。它们之间的依赖关系如下:

graph TD
    subgraph "编译器层 Compiler"
        A["@vue/compiler-core<br/>模板编译核心"] --> B["@vue/compiler-dom<br/>DOM 编译扩展"]
        A --> C["@vue/compiler-sfc<br/>SFC 编译器"]
        A --> D["@vue/compiler-ssr<br/>SSR 编译"]
        A --> V["@vue/compiler-vapor<br/>Vapor 编译器 🆕"]
        B --> C
        D --> C
        V --> C
    end

    subgraph "响应式层 Reactivity"
        E["@vue/reactivity<br/>响应式核心<br/>(Alien Signals 🆕)"]
    end

    subgraph "运行时层 Runtime"
        F["@vue/runtime-core<br/>运行时核心"] --> G["@vue/runtime-dom<br/>DOM 运行时"]
        F --> H["@vue/runtime-vapor<br/>Vapor 运行时 🆕"]
        E --> F
    end

    subgraph "工具层 Utilities"
        I["@vue/shared<br/>共享工具"]
        J["@vue/reactivity-transform<br/>响应式语法糖(已废弃)"]
    end

    subgraph "入口 Entry"
        K["vue<br/>完整构建入口"]
        G --> K
        C --> K
    end

    subgraph "服务端 Server"
        L["@vue/server-renderer<br/>SSR 渲染器"]
        F --> L
    end

    I --> A
    I --> E
    I --> F

    style V fill:#ff6b6b,stroke:#333,color:#fff
    style H fill:#ff6b6b,stroke:#333,color:#fff
    style E fill:#ffd93d,stroke:#333

核心三角

在这 20+ 个包中,有三个构成了 Vue 的核心三角,理解它们之间的协作关系是理解 Vue 全部源码的关键:

                    ┌──────────────┐
                    │   Compiler   │
                    │  模板 → 代码  │
                    └──────┬───────┘

                    生成的代码调用
                    运行时 API

              ┌────────────┴────────────┐
              ↓                         ↓
    ┌──────────────┐          ┌──────────────┐
    │  Reactivity  │ ←──────→ │   Runtime    │
    │  数据追踪     │  依赖驱动  │   DOM 操作   │
    └──────────────┘  更新     └──────────────┘

Compiler(编译器) 将模板转化为可执行代码。在 VDOM 模式下,它生成返回 VNode 的 render 函数;在 Vapor 模式下,它生成直接操作 DOM 的命令式代码。编译器是"翻译官"——它不在运行时执行(构建时已完成),但它的翻译质量直接决定了运行时的性能上限。

Reactivity(响应式) 是 Vue 的数据引擎。它负责追踪数据的变化(refreactivecomputed),并在数据变化时通知依赖者。在 Vue 3.6 中,这个引擎的内核已经被 Alien Signals 完全重写——从 Set-based 追踪变为 version counting。

Runtime(运行时) 是 Vue 的执行引擎。它负责将编译器生成的代码与响应式系统连接起来,管理组件的生命周期(挂载、更新、卸载),处理组件间的通信(props、slots、events),并最终将数据变化反映到 DOM 上。

这三者的协作流程:

  1. 编译期:Compiler 分析模板,识别静态/动态部分,生成优化后的代码
  2. 挂载期:Runtime 执行编译器生成的代码,通过 Reactivity 建立数据-视图的绑定关系
  3. 更新期:Reactivity 检测到数据变化 → 通知 Runtime → Runtime 执行最小化 DOM 更新

你会惊讶地发现,这三个模块在设计上几乎是相互独立的——每一个都可以被替换。Compiler 可以被换成"手写 render 函数"(Vue 支持 JSX);Runtime 可以被换成 Vapor 运行时(本书第 10 章详述);Reactivity 可以被换成 Alien Signals 算法(本书第 3-6 章详述)。三个模块之间通过定义好的接口通信,彼此不依赖内部实现。这种"可插拔的核心"设计是 Vue 3 能实现 Vapor 这种大手术却不破坏现有生态的根本原因——你换掉其中一个模块,另外两个几乎不需要动。

这种架构的启示远不止 Vue 一家:任何一个复杂系统,如果你能把它拆成"几个职责清晰、接口稳定的核心模块",就能在不重写整体的前提下做局部升级。反之,如果系统内部耦合严重,任何一次升级都要动筋骨,最终你只能选择"原地不动"或"彻底重写"——两种极端都是对用户的伤害。Vue 3 的架构设计教给我们的第一课,就是高内聚、低耦合从来不是口号,它决定了一个框架能不能"活得够久"。

🔥 深度洞察

核心三角中最微妙的关系是 Compiler 与 Runtime 的编译期契约。编译器生成的代码不是任意的 JavaScript——它精确地调用 Runtime 暴露的特定 API(如 createElementVNodeopenBlocksetText)。这意味着 Compiler 和 Runtime 之间存在一个隐式的 ABI(Application Binary Interface)。改变 Runtime API 必须同步修改 Compiler 的代码生成逻辑,反之亦然。这就是为什么 Vapor Mode 需要同时新增 @vue/compiler-vapor@vue/runtime-vapor——新的渲染范式需要全新的编译期契约。

各包速览

包名 职责 本书对应章节
@vue/reactivity 响应式核心(ref, reactive, computed, effect) 第 3–6 章
@vue/runtime-core 运行时核心(组件、生命周期、调度器) 第 7–9 章
@vue/runtime-dom DOM 平台特定运行时 第 7 章
@vue/runtime-vapor Vapor 模式运行时 🆕 第 10 章
@vue/compiler-core 模板编译核心(parse → AST → transform → codegen) 第 11–13 章
@vue/compiler-dom DOM 平台特定编译扩展 第 12 章
@vue/compiler-sfc 单文件组件编译(<script setup>, <style scoped> 第 12 章
@vue/compiler-vapor Vapor 模式编译器 🆕 第 10 章
@vue/server-renderer 服务端渲染 附录
@vue/shared 共享工具函数 贯穿全书

1.6 本章小结

本章从宏观视角回答了"为什么在 2026 年重新理解 Vue"这个问题。关键要点:

  1. Vue 经历了三次范式蜕变:Options API(降低门槛)→ Composition API(逻辑复用)→ Vapor Mode(性能天花板突破)。每次蜕变都不是修修补补,而是对核心问题的重新思考。

  2. Vapor Mode 是渲染模型的范式转换:从"生成 VNode 树 → diff → patch"到"编译器直接生成精确的 DOM 操作指令"。复杂度从运行时转移到编译期。

  3. Alien Signals 是响应式模型的范式转换:从 Set-based 的推模型(数据变化 → 立即通知所有依赖者)到 version counting 的拉模型(数据变化 → 递增版本号 → 读取时才检查)。内存和性能均有质的提升。

  4. 旧版源码知识已经过时:如果你的 Vue 源码理解停留在 3.4 或更早,你实际上在学习一个已经被替换的系统。

  5. 编译器-响应式-运行时的核心三角是理解 Vue 全部源码的关键框架。Vapor Mode 的引入需要同时新增编译器和运行时包,因为新范式需要全新的编译期契约。

下一章搭建源码阅读的开发环境——如何在 Vue monorepo 中定位、阅读、调试源码。阅读建议:每章对照打开 Vue 源码文件,章节末尾有源码路径引用,git clone vuejs/core 后 chapter-by-chapter 追踪。

延伸阅读(全书起点)

  • Vue 3 GitHub 仓库 vuejs/core:本书讨论的所有源码的原始出处。强烈建议 git clone 一份到本地,chapter-by-chapter 打开对应文件对照阅读。
  • Evan You 演讲 The State of Vue 系列(2022-2025 Vue Conf):Vue 作者本人对每年 Vue 发展的官方总结,是理解 Vue 方向性的最权威材料。
  • Rich Harris Rethinking Reactivity(2019 YouTube):Svelte 作者对"有没有 VDOM"的第一次公开反思演讲,是理解 Vapor Mode 思路起点的必听材料。
  • Ryan Carniato 博客 The Fundamental Principles of Reactivity:Solid.js 作者对细粒度响应式的系统化论述,和 Alien Signals 的哲学高度相通。
  • Johnson Chu alien-signals GitHub 仓库:Alien Signals 算法的原始实现,开源社区驱动框架进化的当代案例。

思考题

  1. 概念理解:Options API 按"类型"组织代码(data、methods、computed),Composition API 按"功能"组织代码。请举一个具体的业务场景,说明在该场景下 Composition API 的组织方式为什么优于 Options API。

  2. 深入思考:Vapor Mode 将 VNode 的 diff 消除了,但这是否意味着 Vapor 组件在所有场景下都比 VDOM 组件更快?请思考可能存在的例外场景(提示:考虑动态子组件列表和 v-for 的极端情况)。

  3. 横向对比:Svelte 从一开始就没有虚拟 DOM,Solid.js 同样使用细粒度响应式直接更新 DOM。Vue 的 Vapor Mode 与它们的方案有何异同?Vue 选择"渐进式引入 Vapor"(可以混用 VDOM 和 Vapor 组件)这一策略有什么优势和代价?

  4. 工程思考:Alien Signals 的版本计数模型将"推通知"变为"拉检查"。在什么场景下,拉模型可能反而不如推模型?(提示:考虑一个 computed 被频繁读取、但依赖极少变化的场景。)

  5. 开放讨论:回顾 Vue 的三次范式蜕变(Options → Composition → Vapor),你认为下一次蜕变可能发生在哪个层面?编译器会进一步承担什么职责?响应式系统还有哪些可优化的空间?