/packages/complier-core
- 定位:编译时核心,处理 Vue 模板的编译逻辑。
- 核心功能:
- 模板解析:将
.vue
文件的模板语法(HTML-like)解析为 抽象语法树 (AST)。 - 转换优化:静态节点提升(Hoist Static)、Patch Flags 标记(动态节点标记)。
- 代码生成:将 AST 转换为可执行的 渲染函数(
render
函数)。
- 模板解析:将
- 特点:
- 与平台无关,不直接处理浏览器或 DOM,只负责语法层面的转换。
- 支持自定义编译器指令(如
v-xxx
的自定义扩展)。
vue3模板编译原理
模板编译分为三个阶段:
1.解析阶段
该阶段的核心任务是将原始的 HTML 模板字符串转换为初始的抽象语法树(AST)。解析器通过状态机(State Machine)逐字符扫描模板,识别出标签、属性、事件、插值表达式(如 {{ }}
)和组件标签等语法单元。例如,对于 <div :class="cls">{{ text }}</div>
,解析器会拆解出标签名 div
、动态属性 :class
及其绑定表达式 cls
,以及插值内容 text
,最终构建出一棵树形结构的原始 AST。此阶段的 AST 仅保留模板的语法结构,尚未包含任何优化信息,但它为后续的转换和代码生成提供了基础数据结构。
2.转换阶段(transform)
转换阶段是 Vue3 编译流程的核心优化环节,通过对 AST 的多轮遍历,实现语义分析和结构改造。这一阶段包含多项关键技术:
1. Node Transforms 多步处理
作用:通过一系列转换函数(如 transformIf
、transformFor
)逐层处理 AST 节点,将模板声明式语法转换为可执行的 JavaScript 逻辑。
技术细节:
- 递归遍历 AST,依次应用
v-if
→ 条件表达式、v-for
→ 循环结构等转换规则 - 动态绑定分析(如识别
:class
和{{ text }}
的动态性差异) - 为后续优化阶段(如静态提升)提供结构化标记
2. 静态提升(Static Hoisting):识别纯静态内容(如无动态绑定的 <div>Static</div>
),将其提取为常量并复用,避免重复创建虚拟节点。
3. Patch Flags 标记:为动态节点添加二进制标志(如 CLASS | STYLE | PROPS
),指导运行时仅对比变化的属性,跳过全量 Diff。例如,动态绑定的 :class
和 {{ text }}
会被标记为不同的 Patch Flag。
4. 块追踪(Block Tracking):将包含动态子节点的父节点标记为“块”(Block),运行时通过 openBlock()(标记一个动态块的开始,开始收集后续动态节点)
和 closeBlock()
(通过createBlock()
自动关闭块,无需显式调用)控制更新范围,缩小虚拟 DOM 的比对粒度。
5. 缓存优化:对事件处理函数和动态属性值进行逻辑赋值缓存(如 _cache[0] || (_cache[0] = ...)
),避免重复创建函数或计算表达式。
这些优化使得 AST 从“原始语法描述”升级为“富含运行时提示的中间形态”,为生成高效代码奠定了基础。
3.生成阶段
生成阶段将优化后的 AST 转换为可执行的渲染函数代码。代码生成器遍历 AST 节点,根据节点类型和优化标记拼接字符串,最终输出类似 function render() { ... }
的 JavaScript 函数。例如,一个包含静态提升和动态绑定的模板会被编译为:
const _hoisted_1 = createVNode("div", null, "Static");
function render() { return (openBlock(), createBlock("div", null, [ _hoisted_1, createVNode("p", null, ctx.text, 1 /* TEXT */) ]));
}
此阶段还会注入运行时代码(如 createVNode
、openBlock
),并确保生成的代码符合 JavaScript 语法规范。最终输出的渲染函数与 Vue 的运行时协作,在组件渲染时高效生成和更新虚拟 DOM。
/packages/reactivity
- 定位:响应式系统的独立实现,Vue3 的「数据驱动」核心。
- 核心功能:
- 依赖追踪:通过
Proxy
+Reflect
实现数据的响应式代理。 - 核心 API:
reactive
:创建深度响应式对象。ref
:包装基本类型值为响应式引用。effect
:副作用函数(替代 Vue2 的 Watcher)。computed
:计算属性。
- 依赖收集与触发:通过
track
和trigger
管理依赖关系。
- 依赖追踪:通过
proxy通过new Proxy(target,handler) 生成,new Proxy是JS原生API,用于创建一个代理对象,这个代理对象会包装原始对象(target),并允许通过拦截器(handler)定义目标对象的操作行为
Proxy
一、new Proxy
的核心作用
1. 创建代理包装器
- 输入:一个原始对象
target
和一个拦截器对象handler
- 输出:生成一个代理对象(Proxy Instance),该代理对象会转发所有操作到原始对象,但允许通过
handler
拦截特定操作
const raw = { a: 1 };
const handler = {get(target, key) {console.log(`读取属性 ${key}`);return target[key];}
};
const proxy = new Proxy(raw, handler);console.log(proxy.a); // 输出 "读取属性 a",然后输出 1
2. 定义拦截行为
handler
对象中可定义陷阱函数(Traps),用于拦截 13 种对象操作(如get
、set
、deleteProperty
等)- 当对 Proxy 实例进行操作时,会优先触发对应的陷阱函数,未定义的陷阱会直接转发到原始对象
二、生成的 Proxy 实例特点
1. 透明访问性
- 镜像原始对象:Proxy 实例的属性访问、方法调用等行为会默认转发到
target
const raw = { x: 10 }; const proxy = new Proxy(raw, {}); console.log(proxy.x); // 10(未定义陷阱,直接访问原始对象)
2. 操作可拦截性
- 精细拦截:通过陷阱函数可拦截以下操作:
陷阱函数 拦截的操作 get
读取属性(如 obj.prop
)set
设置属性(如 obj.prop = 1
)has
in
操作符(如'prop' in obj
)deleteProperty
delete
操作符(如delete obj.prop
)apply
函数调用(如 fn()
)construct
new
操作符(如new Fn()
)ownKeys
Object.keys()
、for-in
循环等
3. 引用独立性
- 独立对象:Proxy 实例是一个独立的对象,与
target
是不同引用const raw = {}; const proxy = new Proxy(raw, {}); console.log(proxy === raw); // false
4. 动态关联性
- 实时关联:Proxy 实例的操作会实时影响原始对象
const raw = { a: 1 }; const proxy = new Proxy(raw, {}); proxy.a = 2; console.log(raw.a); // 2(修改代理实例会影响原始对象)
5. 不可变性
- 代理不可逆:无法通过 Proxy 实例直接获取原始的
handler
或target
(需通过特殊属性或方法)const proxy = new Proxy({}, {}); console.log(proxy); // 控制台输出 Proxy 对象,但无法直接看到 handler 和 target
三、在 Vue 3 中的特殊设计
在 packages/reactivity
模块中,Vue 3 的 reactive()
函数生成的 Proxy 实例有以下特点:
1. 响应式触发器
get
陷阱:触发依赖收集(track
)set
陷阱:触发更新通知(trigger
)
2. 嵌套对象懒代理
- 仅在访问嵌套属性时,才递归生成子 Proxy 对象
const obj = reactive({ nested: { a: 1 } // 此时 nested 未被代理 }); console.log(obj.nested); // 访问时生成嵌套 Proxy
3. 原始对象标记
- 通过
ReactiveFlags.RAW
标记可获取原始对象
四、与普通对象的对比
特性 | 普通对象 | Proxy 实例 |
---|---|---|
动态属性拦截 | 不支持 | 支持(如新增属性、删除属性) |
引用关系 | 独立对象 | 独立对象,但操作关联原始对象 |
操作拦截能力 | 无 | 可拦截 13 种操作 |
性能开销 | 无 | 微小(但频繁操作可能累积开销) |
五、示例:实现一个简易响应式
const handler = {get(target, key) {console.log(`读取 ${key}`);return target[key];},set(target, key, value) {console.log(`设置 ${key} 为 ${value}`);target[key] = value;return true;}
};const raw = { count: 0 };
const proxy = new Proxy(raw, handler);proxy.count++; // 输出 "读取 count",然后 "设置 count 为 1"
Reflect
一、Reflect
的本质
Reflect
是 ES6 引入的全局对象,提供了一套与对象操作方法一一对应的函数式 API。例如:
Reflect.get(target, key)
↔ 对应target[key]
Reflect.set(target, key, value)
↔ 对应target[key] = value
Reflect.has(target, key)
↔ 对应key in target
其核心设计目标是规范化对象操作,使得原本隐式的操作(如属性读写、in
操作符)可以通过函数显式调用。
二、在 Proxy 陷阱中的必要性
当使用 Proxy
时,必须通过 Reflect
方法转发默认操作,否则会导致原始对象行为的丢失。以下是关键原因:
1. 保持 receiver
上下文正确性
在访问器属性(getter/setter)中,this
的指向需要正确绑定到代理对象(而非原始对象)。
通过 Reflect.get(target, key, receiver)
的第三个参数 receiver
,可以确保 this
指向代理对象。
示例:
const raw = {_value: 0,get value() {return this._value; // this 必须指向代理对象}
};const proxy = new Proxy(raw, {get(target, key, receiver) {// 使用 Reflect 传递 receiverreturn Reflect.get(target, key, receiver);}
});console.log(proxy.value); // 正确触发代理的 get 陷阱
2. 返回值标准化
Reflect
方法返回布尔值或操作结果,与 Proxy 陷阱的返回值要求一致。例如:
Reflect.set
返回boolean
表示是否设置成功Reflect.deleteProperty
返回boolean
表示是否删除成功
示例:
const proxy = new Proxy({}, {set(target, key, value, receiver) {// 必须返回布尔值return Reflect.set(target, key, value, receiver);}
});
3. 避免破坏对象内部方法
直接操作 target[key]
可能绕过对象内部的 [[Get]]
、[[Set]]
等内部方法,而 Reflect
确保操作符合规范。
在 JavaScript 中,直接通过
target[key]
操作对象属性可能会绕过对象内部的[[Get]]
和[[Set]]
等内部方法,原因如下:
一、
[[Get]]
和[[Set]]
的本质对象的属性访问(如
obj.prop
)和赋值(如obj.prop = value
)本质上会触发对象内部的[[Get]]
和[[Set]]
方法。这些内部方法负责:
- 访问器属性(getter/setter)的执行
- 原型链的查找
- 属性描述符(如
writable
、configurable
)的校验
二、直接操作
target[key]
的问题当在 Proxy 的陷阱函数中直接操作
target[key]
时,可能绕过以下关键逻辑:1. 绕过访问器属性(getter/setter)
如果目标对象(
target
)的属性是访问器属性(定义了get
或set
),直接通过target[key]
访问或赋值会跳过访问器逻辑,直接操作底层值。示例:
const obj = {get value() {console.log("getter 被调用");return this._value;},set value(v) {console.log("setter 被调用");this._value = v;} };const proxy = new Proxy(obj, {get(target, key) {// 直接返回 target[key],绕过 getterreturn target[key]; // ❌ 错误方式},set(target, key, value) {// 直接赋值 target[key],绕过 settertarget[key] = value; // ❌ 错误方式return true;} });proxy.value = 1; // 不会触发 setter 的 console.log console.log(proxy.value); // 不会触发 getter 的 console.log
- 结果:直接操作
target[key]
会导致访问器逻辑被绕过,_value
被直接读写。2. 破坏
this
绑定访问器属性中的
this
默认指向原始对象(target
),而非代理对象(proxy
)。这会导致依赖代理对象的逻辑(如嵌套响应式)失效。示例:
const raw = {get value() {return this._value; // this 指向 raw,而非 proxy} };const proxy = new Proxy(raw, {get(target, key) {return target[key]; // ❌ this 错误绑定到 raw} });proxy._value = 42; console.log(proxy.value); // 42,但若 proxy 有拦截逻辑,this 指向错误会导致问题
3. 绕过嵌套 Proxy
如果目标对象本身是另一个 Proxy,直接操作
target[key]
会绕过其陷阱函数,直接访问原始值。示例:
const inner = new Proxy({ a: 1 }, {get(target, key) {console.log("inner 的 get 陷阱");return Reflect.get(target, key);} });const outer = new Proxy(inner, {get(target, key) {// 直接访问 target[key],绕过 inner 的 Proxy 陷阱return target[key]; // ❌ 不会触发 inner 的 get 陷阱} });console.log(outer.a); // 直接输出 1,无日志
4. 忽略原型链和属性描述符
直接操作
target[key]
可能绕过原型链查找和属性描述符(如writable
)的校验。示例:
const parent = { a: 1 }; const child = Object.create(parent); const proxy = new Proxy(child, {get(target, key) {return target[key]; // ❌ 直接返回 undefined(不会查找原型链)} });console.log(proxy.a); // undefined(正确应为 1)
三、为什么
Reflect
能避免这些问题?
Reflect
方法(如Reflect.get
)会严格遵循对象的[[Get]]
和[[Set]]
内部方法,确保以下行为:1. 触发访问器属性
const proxy = new Proxy(obj, {get(target, key, receiver) {return Reflect.get(target, key, receiver); // ✅ 触发 getter},set(target, key, value, receiver) {return Reflect.set(target, key, value, receiver); // ✅ 触发 setter} });
2. 绑定正确的
this
(通过receiver
参数)Reflect.get(target, key, receiver);
receiver
参数确保访问器属性中的this
指向代理对象(而非原始对象)。3. 处理原型链和属性描述符
Reflect.get(target, key, receiver); // ✅ 沿原型链查找属性 Reflect.set(target, key, value, receiver); // ✅ 校验 `writable` 等属性
4. 支持嵌套 Proxy
// 通过 Reflect 触发内层 Proxy 的陷阱 Reflect.get(innerProxy, key, receiver); // ✅ 触发 innerProxy 的 get 陷阱
四、关键对比
操作方式 直接操作 target[key]
使用 Reflect
访问器属性 绕过 getter/setter 正确触发 getter/setter this
绑定指向原始对象( target
)指向代理对象(通过 receiver
)原型链 可能中断查找 完整执行原型链查找 嵌套 Proxy 绕过内层 Proxy 的陷阱 触发内层 Proxy 的陷阱 属性描述符校验 忽略 writable
、configurable
等严格校验
三、在 Vue 响应式系统中的具体应用
Vue 3 的 reactive()
函数通过 Proxy
+ Reflect
实现响应式代理。以下是核心场景:
1. 依赖收集(Track)
在 get
陷阱中,通过 Reflect.get
获取值,并收集依赖:
function createGetter() {return function get(target: object, key: string | symbol, receiver: object) {// 使用 Reflect.get 获取原始值const res = Reflect.get(target, key, receiver);// 依赖追踪track(target, TrackOpTypes.GET, key);// 递归代理嵌套对象if (isObject(res)) {return reactive(res);}return res;};
}
2. 触发更新(Trigger)
在 set
陷阱中,通过 Reflect.set
设置值,并触发更新:
function createSetter() {return function set(target: object,key: string | symbol,value: unknown,receiver: object): boolean {// 获取旧值const oldValue = (target as any)[key];// 使用 Reflect.set 设置值const result = Reflect.set(target, key, value, receiver);// 触发更新(仅当值变化时)if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key);}return result;};
}
3. 处理 has
和 ownKeys
陷阱
拦截 in
操作符和 Object.keys()
:
四、Reflect
与直接操作对象的区别
操作方式 | 直接操作对象(如 target[key] ) | 使用 Reflect |
---|---|---|
this 绑定 | this 指向原始对象 | 通过 receiver 参数控制 this 指向 |
返回值 | 无标准返回值(可能抛出错误) | 标准化布尔值或操作结果 |
原型链处理 | 可能无法正确处理继承属性 | 严格遵循对象原型链规则 |
函数式调用 | 无法以函数形式调用 | 函数式 API,便于组合和抽象 |
五、关键设计思想总结
-
行为一致性
Reflect
方法严格遵循 ECMAScript 规范,确保代理对象的行为与原对象一致。 -
代理链完整性
在多层代理(如 Vue 的readonly(reactive(obj))
)中,Reflect
能正确传递receiver
,维护代理链的上下文。 -
错误处理简化
通过Reflect
的布尔返回值,避免手动处理异常(如设置不可写属性时的TypeError
)。
示例:没有 Reflect
的陷阱
若在 Proxy 陷阱中不使用 Reflect
,会导致意外行为:
const raw = { a: 1 };
const proxy = new Proxy(raw, {get(target, key) {// 错误!未使用 Reflect.get,this 指向错误return target[key];},set(target, key, value) {// 错误!未返回布尔值target[key] = value;return true;}
});const child = { __proto__: proxy };
console.log(child.a); // 期望触发 get 陷阱,但实际不会!
/packages/runtime-core
- 定位:运行时核心,实现 Vue 组件的生命周期、虚拟 DOM、渲染调度等。
- 核心功能:
- 虚拟 DOM 算法:Diff 算法(
patchKeyedChildren
)、节点 Patch 逻辑。 - 组件系统:组件实例化、Props/Emits 处理、Slots 管理。
- 生命周期钩子:
onMounted
、onUpdated
等 Composition API 的实现。 - 自定义渲染器:提供
createRenderer
API,支持跨平台渲染(如小程序、Canvas)。
- 虚拟 DOM 算法:Diff 算法(
- 特点:
- 平台无关,不直接操作 DOM,依赖外部传入的 宿主环境 API。
- 包含 Vue 的核心逻辑,但需与
runtime-dom
结合才能用于浏览器。
/packages/runtime-dom
- 定位:浏览器端运行时,提供 DOM 相关的原生操作。
- 核心功能:
- DOM 操作 API:封装
document.createElement
、parent.appendChild
等原生方法。 - 事件处理:标准化事件监听(如
onClick
的自动转换)。 - 属性处理:处理 HTML Attributes、DOM Properties、类名、样式等。
- DOM 操作 API:封装
- 特点:
- 基于
runtime-core
实现,是 Vue 在浏览器环境下的 默认渲染器。 - 导出
createApp
等浏览器端专用 API。
- 基于
/packages/runtime-test
- 定位:测试用运行时,用于单元测试场景。
- 核心功能:
- 轻量级 DOM 模拟:提供内存中的虚拟 DOM 操作,避免依赖真实浏览器环境。
- 序列化输出:将渲染结果序列化为字符串,方便断言(Assertion)。
- 生命周期追踪:记录组件生命周期钩子的调用顺序。
- 使用场景:
- 测试组件渲染逻辑,不依赖
jsdom
或浏览器。 - 验证虚拟 DOM 的 Diff 算法是否正确。
- 测试组件渲染逻辑,不依赖