Vue3 发布以来,凭借其卓越的性能表现和优雅的架构设计,赢得了大量开发者的青睐。然而,多数开发者对 Vue3 的认知仍停留在 API 使用层面,对其底层实现机制缺乏系统性理解。本文将深入 Vue3 源码,从编译层、运行时层到响应式系统,完整剖析 Vue3 从模板到 DOM 更新的全链路实现。
一、项目架构概览:模块化设计的哲学
Vue3 源码采用 monorepo 架构管理,核心代码集中在 packages/ 目录下。整个框架被划分为几个核心模块:reactivity/ 负责响应式系统、runtime-core/ 与 runtime-dom/ 负责运行时渲染、compiler-core/ 与 compiler-dom/ 负责模板编译。这种高度解耦的设计使得各个模块可以独立使用------比如开发者可以单独引入 @vue/reactivity 包,在非 Vue 项目中也享受响应式能力。
从宏观视角看,Vue3 的运行可以概括为三个阶段:编译 (模板 → 渲染函数)、渲染 (渲染函数 → 虚拟 DOM)、更新(响应式数据变化 → 重新渲染)。下面我们逐一剖析每个阶段的底层实现。
二、编译层:模板到渲染函数的转换引擎
编译层是 Vue3 实现响应式 UI 的核心预处理阶段。当开发者编写 .vue 文件或模板字符串时,Vue3 的编译器会将其转换为可执行的渲染函数。
2.1 编译的三阶段流水线
Vue3 的编译过程分为三个核心阶段:parse(解析) 、transform(转换) 和 generate(代码生成) 。
入口函数 compile 定义在 compiler-dom 中,其内部实际调用的是 compiler-core 的 baseCompile 函数:
javascript
scss
// 精简自 compiler-core/src/compile.ts
export function baseCompile(template, options) {
// 阶段1:解析模板生成 AST
const ast = baseParse(template, options);
// 阶段2:转换 AST(优化标记)
transform(ast, options);
// 阶段3:生成渲染函数代码
return generate(ast, options);
}
Parse 阶段 将模板字符串解析为抽象语法树(AST)。AST 本质上是一个 JavaScript 对象,用于结构化描述模板内容------每个节点包含 type(节点类型)、tag(标签名)、children(子节点)等属性。
Transform 阶段 在 AST 上增加各类优化标记。Vue3 会在此阶段识别静态节点------即渲染过程中不会变化的节点,并为其打上标记。这些标记在后续的 Diff 过程中帮助跳过不必要的对比,是 Vue3 性能优化的重要一环。
Generate 阶段 根据优化后的 AST 生成渲染函数字符串。编译后的代码通过 _ctx 对象访问组件实例数据,实现高效的运行时调用。
2.2 编译优化:静态提升
Vue3 重写了整个编译器,提出了静态提升 等优化策略。静态提升的核心思想是:将编译阶段识别出的静态节点提升到渲染函数外部,避免每次重新渲染时重复创建这些节点。例如模板中的 <div>静态内容</div> 在编译后会被提升为常量,渲染函数每次执行时直接引用而非重新创建。这一优化显著减少了虚拟 DOM 创建的开销。
三、运行时层:虚拟 DOM 与高效更新
编译完成后,生成的渲染函数会在运行时被执行,产出虚拟 DOM 树(VNode 树)。Vue3 的运行时层由 runtime-core 和 runtime-dom 两个模块协同完成。
3.1 虚拟 DOM 的核心数据结构
type:节点类型(如'div'、组件对象)props:属性对象children:子节点key:唯一标识,用于 Diff 优化patchFlag:Vue3 特有的动态属性标记
patchFlag 是 Vue3 的关键创新------编译阶段为每个节点标记其动态属性类型(如 CLASS、PROPS、TEXT),运行时 Diff 时可据此跳过静态内容的对比。
3.2 快速 Diff 算法
当响应式数据发生变化时,Vue3 需要将新旧两棵 VNode 树进行对比,找出最小化的 DOM 操作指令。Vue3 采用快速 Diff 算法 ,将时间复杂度从 O(n³) 优化至接近 O(n)。
核心实现在 patchKeyedChildren 函数中,其策略可以概括为:
- 头头对比:从新旧列表的头部开始,逐个对比节点,若类型和 key 相同则复用。
- 尾尾对比:从新旧列表的尾部开始,逐个对比节点。
- 交叉对比:处理头尾、尾头等交叉场景。
- Key 映射:对剩余节点建立 key 到索引的映射表,快速定位可复用节点。
- 最长递增子序列(LIS) :对于无 key 的列表,通过 LIS 算法计算最少移动次数。
key 在 Diff 中扮演着关键角色------通过 key 可以精准匹配新旧节点,避免因顺序变化导致的错误复用。
四、响应式系统:数据驱动更新的核心引擎
响应式系统是 Vue3 最核心的底层机制。当数据变化时,系统需要知道"哪些代码依赖了这个数据"并触发相应的更新。
4.1 Proxy:数据劫持的现代实现
Vue2 使用 Object.defineProperty 实现数据劫持,但存在三大局限:无法监听数组索引变化、无法检测新增/删除属性、需要递归遍历所有属性。Vue3 转而使用 ES6 的 Proxy,提供了更强大且灵活的拦截能力:
javascript
javascript
// reactive 的核心实现(精简自 reactivity 源码)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
}
});
}
Proxy 不仅可以监听数组的所有操作(包括 push、pop、splice 等),还能在访问和修改属性时动态拦截,无需预先定义所有响应式属性。
4.2 依赖收集与触发:track 与 trigger
响应式系统的核心在于建立数据属性 与副作用函数(Effect) 之间的动态关联。依赖关系通过三层数据结构存储:
text
scss
targetMap (WeakMap) → target (对象) → depsMap (Map) → key (属性名) → dep (Set) → activeEffect (副作用函数)
targetMap 使用 WeakMap 存储,既保证了内存管理的有效性(允许垃圾回收),又提供了 O(1) 的查找效率。
依赖收集(track) 的过程是:当响应式数据被读取时(触发 Proxy 的 get 拦截),将当前正在执行的副作用函数 activeEffect 添加到该属性对应的 dep 集合中。
派发更新(trigger) 的过程是:当响应式数据被修改时(触发 Proxy 的 set 拦截),从 targetMap 中取出该属性对应的所有副作用函数,依次执行。
4.3 Effect 系统:副作用的管理与调度
effect 函数是响应式系统的调度核心。当调用 effect(fn) 时,内部会创建 ReactiveEffect 对象:
javascript
ini
export function effect(fn, options) {
const _effect = new ReactiveEffect(fn);
if (!options?.lazy) {
_effect.run(); // 立即执行,触发依赖收集
}
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
ReactiveEffect 类的 run 方法执行时,会将自身设置为全局的 activeEffect,然后执行副作用函数。执行过程中访问到的所有响应式属性都会通过 track 将当前的 activeEffect 收集为依赖。
Vue3 还支持 effect 嵌套 ,通过 effectStack 栈来维护嵌套的 effect 上下文。对于频繁触发更新的场景,Vue3 通过批量更新 机制(使用微任务队列合并同类更新)和优先级调度 来优化性能。
4.4 懒加载响应式:按需代理
Vue3 在响应式代理上还有一个重要优化:当新建响应式数据时,Vue 只会对对象最外层属性创建代理;对于深层属性,只有在该属性被读取时才会为其创建对应的 Proxy。反观 Vue2,无论 state 对象多大、嵌套多深,都需要在一开始进行遍历,为每个属性定义 getter 和 setter。这种按需代理的策略大幅提升了大型对象的初始化性能。
五、全链路闭环:从数据变化到视图更新
- 编译时:模板 → AST → 优化标记 → 渲染函数
- 首次渲染:渲染函数执行 → 创建 VNode 树 → 挂载真实 DOM
- 响应式绑定 :渲染过程中访问响应式数据 →
track收集渲染函数为依赖 - 数据变化 :用户修改数据 → Proxy 的
set拦截 →trigger触发依赖 - 重新渲染:渲染函数重新执行 → 生成新 VNode 树 → Diff 对比 → 最小化 DOM 更新