Vue3源码

Vue3源码
匡思进
框架
Vue 3 的框架结构相比 Vue 2 进行了彻底的重构,使其更加模块化、高性能和 Tree-shaking 友好。
Vue 3 的核心不再是一个庞大的单体结构,而是由一系列职责清晰的模块组成的,这种设计被称为 “Monorepo” 架构。
Vue 3 的模块化框架结构
Vue 3 的框架主要由以下几个核心包组成,它们协同工作,构成了 Vue 的运行时环境:
1. 响应式系统 (@vue/reactivity)
核心作用: 这是 Vue 3 最底层也是最核心的机制。它负责跟踪状态的变化并触发视图更新。
技术基础: 使用 JavaScript Proxy 实现。
主要 API: 提供了
reactive、ref、readonly、computed、watch等函数,用于创建和管理响应式状态。优势: 它是完全独立的,可以被用于任何 JavaScript 环境中,与 DOM 解耦。
2. 运行时核心 (@vue/runtime-core)
核心作用: 包含创建、管理和更新虚拟 DOM (VNode) 的所有逻辑。它是平台无关的 Vue 核心逻辑。
主要职责: 实现组件的生命周期、组合式 API 的基础实现、调度器(负责控制更新频率)和 Diff 算法等。
平台定制: 它提供了创建特定平台渲染器的 API。
3. 运行时渲染器 (@vue/runtime-dom)
核心作用: 这是一个平台特定的渲染器,专门用于浏览器 DOM 环境。
职责: 它基于
runtime-core,提供了将 VNode 真正渲染到浏览器 DOM 上的逻辑,包括处理 DOM 元素的创建、属性的设置、事件监听器的绑定等。在 Vue 应用中的作用: 当我们使用 Vue.createApp() 时,内部使用的就是这个 runtime-dom 渲染器。
4. 编译器 (@vue/compiler-core / @vue/compiler-dom)
核心作用: 负责将 Vue 的**模板(Template)**编译成高效的 VNode 渲染函数 (Render Function)。
关键优化: 编译器在 Vue 3 中引入了**块树(Block Tree)和静态提升(Static Hoisting)**等优化技术,极大地提升了 VNode 渲染函数的性能。
职责分工:
@vue/compiler-core:平台无关的模板解析和代码生成逻辑。@vue/compiler-dom:增加了处理 DOM 特有指令(如 v-bind、v-on)的逻辑。
5. 共享模块 (@vue/shared)
- 核心作用: 存放 Vue 框架中所有模块共用的工具函数和常量(如类型检查、对象操作函数等)。
整体工作流程
Vue 3 框架的工作流程可以概括为:
编译期 (Build Time 或 Runtime): 模板 通过
@vue/compiler-*编译成 VNode 渲染函数。挂载期 (Mount): 渲染函数 被执行,创建 VNode 树,然后
@vue/runtime-dom将 VNode 树转化为真实的 DOM 元素并插入页面。响应式更新期 (Update): 当
@vue/reactivity模块监测到状态变化时,它会通知@vue/runtime-core执行更新操作,通过高效的 Diff 算法计算出最小的更新集合,最后由@vue/runtime-dom精确地修改真实的 DOM。
这种模块化的设计使得 Vue 3 的核心体积更小,可维护性更强,并且为将 Vue 渲染到非 DOM 环境(如 Weex、原生应用)提供了天然的优势。
Monorepo 管理项目
Monorepo 是管理项目代码的一个方式, 指在一个项目仓库中管理多个模块 / 包. Vue3源码采用monorepo方式进行管理, 将模块拆分到package目录中. 作为一个个包管理, 这样职责划分更加明确
- 一个仓库可维护多个模块
- 方便版本管理和依赖管理
Reactivity — 响应式实现
![[前端了解/vue源码/_resources/设计思想/3a9a8fed4fa432ee61e5d6e53f9fb7df_MD5.png]]
![[前端了解/vue源码/_resources/设计思想/3583abb2dc9d6f8d378bf565f20b900a_MD5.png]]
![[前端了解/vue源码/_resources/设计思想/d88cbe6d4b85fbc2286e9b027c1465d6_MD5.png]]
reactivity
1. 缓存机制 (reactiveMap)
1 | |
作用:
reactiveMap用于存储 原始对象(target)和其对应的 Proxy 实例。用 WeakMap的原因 使用 WeakMap 而不是普通
Map的关键在于它的弱引用特性。如果原始对象不再被其他地方引用,WeakMap 不会阻止垃圾回收(GC)机制回收该对象及其对应的 Proxy,这有效防止了内存泄漏。缓存目的: 确保同一个原始对象不会被重复代理,总是返回同一个 Proxy 实例。
2. 响应式标记 (ReactiveFlags)
1 | |
作用: 定义一个特殊的、不常用的 Symbol 或字符串键 (
__v_isReactive),用于标记一个对象是否已经是响应式代理。实现方式: 通过 Proxy 的
get捕获器,在访问这个特殊键时返回true。
3. 核心代理句柄 (mutableHandlers)
当一个对象被代理后,任何对它的属性访问都会经过这里的 get 捕获器:
1 | |
- 如果尝试读取 Proxy 上的
__v_isReactive属性,get捕获器会立即返回true - 如果对一个原始对象读取这个属性,它会返回
undefined(或者如果原始对象恰好有这个属性,则返回属性值,但一般不会)
使用Reflect的好处
确保正确的 this 指向 (Receiver 参数)
这是使用 Reflect.get() 和 Reflect.set() 的最主要原因。
在 get 捕获器中使用 return target[key] 的传统方式时,如果 target 对象上的属性是一个 getter 或 setter 函数,那么这个函数内部的 this 将指向原始目标对象 (target),而不是我们期望的代理对象 (proxy)。
这会破坏响应式:如果 getter/setter 内部又访问了目标对象上的其他属性,这个访问操作会绕过 Proxy,无法触发响应式机制(如依赖收集)。
4. 响应式工厂 (createReactiveObject)
这是核心逻辑所在,确保了响应式对象创建的规范性、唯一性和复用性。
| 代码行 | 目的 | 详细解释 |
|---|---|---|
if (!isObject(target)) |
非对象校验 | 响应式只能代理对象(包括数组)。非对象类型直接返回。 |
if(target[ReactiveFlags.IS_REACTIVE]) |
重复代理校验 | 如果目标对象已经是 Proxy(通过访问特殊标记判断),则直接返回它自身,避免重复创建 Proxy。 |
const existProxy = reactiveMap.get(target) |
缓存查找 | 检查当前原始对象是否在 reactiveMap 中已有对应的 Proxy。 |
if(existProxy) { return existProxy } |
返回缓存 | 如果找到缓存,直接返回已存在的 Proxy 实例,确保唯一性。 |
const proxy = new Proxy(target, mutableHandlers) |
创建 Proxy | 只有通过所有检查后,才创建新的 Proxy 实例。 |
reactiveMap.set(target, proxy) |
缓存新实例 | 将 原始对象 和 新 Proxy 存入 WeakMap,供将来复用。 |
effect
一、核心作用和结构
effect 函数用于创建一个响应式副作用(Reactive Effect),它是连接“数据变化”与“重新执行某个函数”的桥梁。
| 元素 | 作用 | 对应代码 |
|---|---|---|
effect(fn) |
创建并执行一个副作用。它接收一个函数 fn(即要执行的副作用),并立即执行一次,同时建立响应式关联。 |
export function effect(fn, options?) |
ReactiveEffect |
副作用的封装类。它存储了副作用函数 fn、调度器 scheduler,以及用于依赖追踪和清理的状态(deps、_trackedId 等)。 |
class ReactiveEffect { ... } |
activeEffect |
全局变量,用于临时存储当前正在运行的 ReactiveEffect 实例。这是依赖收集的关键。 |
export let activeEffect |
二、 ReactiveEffect.run() 的执行流程 (依赖收集与清理)
run() 方法是 ReactiveEffect 的核心,它控制着副作用函数的执行,并在执行前后管理依赖的收集和清理。
| 步骤 | 目的 | 对应函数/代码 |
|---|---|---|
| 1. 激活与缓存 | 检查是否处于激活状态。备份上一个 activeEffect,并将当前 effect 设为 activeEffect。 |
activeEffect = this |
| 2. 依赖预清理 | 在 fn 运行前,重置依赖追踪状态,为新的收集做准备。 |
proCleanEffect(this) |
3. 执行 fn |
运行用户传入的副作用函数。在这个过程中,如果访问了响应式数据,就会触发 get 拦截,进而调用 track 进行依赖收集。 |
return this.fn() |
| 4. 依赖后清理 | 在 fn 运行后,清理上次遗留的、本次执行中没有再次被访问到的旧依赖(解决了 if 条件分支的依赖残留)。 |
postCleanEffect(this) |
| 5. 恢复 | 恢复上一个 activeEffect。 |
activeEffect = lastEffect |
三、 依赖的精确追踪机制(cleanup)
为了解决条件分支(如 if (state.flag))导致的依赖冗余问题,代码实现了一套精确的依赖清理机制,核心在于 ReactiveEffect 实例上的三个属性:_trackedId、deps 和 _depsLength。
1. 预清理 (proCleanEffect)
| 属性 | 变化 | 目的 |
|---|---|---|
_depsLength |
重置为 0 | 记录本次 run 过程中实际收集到的依赖数量。 |
_trackedId |
递增(++) |
为本次 run 生成一个唯一的 ID,用于标记在本次执行中被访问过的依赖。 |
2. 收集 (trackEffect 中的逻辑)
在 trackEffect 中,通过检查和更新 dep 集合、effect.deps 数组来建立和维护双向链接。
| 检查点 | 目的 | 关键代码 |
|---|---|---|
| 去重与标记 | 将当前 effect 和本次 _trackedId 存入 dep 中。如果 effect 已经在 dep 中,且 ID 相同(同一个 run 中多次访问),则跳过。 |
if (dep.get(effect) !== effect._trackedId) { dep.set(effect, effect._trackedId) } |
| 双向链接 | 将 dep 存入 effect.deps 数组中。如果当前位置 (effect.deps[effect._depsLength]) 上存的不是本次的 dep,则说明依赖发生了变化,需要先通过 cleanDepEffect 删除旧依赖,再存入新依赖。 |
if (oldDep !== dep) { ... cleanDepEffect(oldDep, effect) ... effect.deps[effect._depsLength++] = dep } |
| 计数 | 无论是否更换依赖,只要成功收集了一个依赖,_depsLength 就会递增。 |
effect._depsLength++ |
3. 后清理 (postCleanEffect)
| 检查点 | 目的 | 关键代码 |
|---|---|---|
| 比较长度 | 比较上次收集的长度 (deps.length) 和本次实际收集的长度 (_depsLength)。 |
if (effect.deps.length > effect._depsLength) |
| 清理冗余 | 遍历所有多余的旧依赖(deps[_depsLength] 到 deps[length - 1]),调用 cleanDepEffect 将 effect 从这些 dep 中删除。 |
for (let i = effect._depsLength; i < effect.deps.length; ++i) { cleanDepEffect(effect.deps[i], effect) } |
| 截断 | 将 effect.deps 数组的长度截断为本次实际收集的长度。 |
effect.deps.length = effect._depsLength |
四、 依赖触发 (trigger 和 trackEffects)
| 元素 | 作用 |
|---|---|
trigger(target, key, ...) |
在 set 拦截器中被调用。它通过 target 和 key 查找到对应的 dep 集合。 |
trackEffects(dep) |
遍历 dep(存储了所有依赖该属性的 effect),并执行它们的 scheduler 函数(在你的代码中,scheduler 被定义为 () => _effect.run(),即重新执行副作用)。 |
这种设计确保了只有当响应式数据发生变化时,才会执行那些依赖于该数据的副作用函数。通过精确的依赖清理机制,它避免了在条件分支等情况下产生不必要的、无法自动删除的依赖残留。
ref
ref (RefImpl) 实现:基本类型响应化
ref 是将一个值包装成一个带有 .value 属性的引用对象(RefImpl),从而使其具备响应式能力。
核心结构:RefImpl
| 属性/方法 | 作用 | 分析 |
|---|---|---|
__v_isRef |
标识 | 标记这是一个 ref 对象,用于 proxyRefs 和其他内部判断。 |
rawValue |
原始值 | 存储传入的原始值。 |
_value |
内部值 | 存储可能被 toReactive 处理后的响应式值。注意: 你的实现中使用了 this._value = toReactive(rawValue),这实现了 Ref 内部的深度响应(即如果 ref 传入一个对象,该对象会被 reactive 包裹)。 |
dep |
依赖集合 | 用于存储依赖于这个 ref 值的 effect 集合。 |
get value() |
依赖收集 | 访问时调用 trackRefValue 进行依赖收集,然后返回 this._value。 |
set value(newValue) |
触发更新 | 比较新值和旧值。如果不同,更新 rawValue 和 _value,然后调用 triggerRefValue 触发所有相关的 effect 重新执行。 |
依赖追踪
trackRefValue(ref):在get value()时调用。如果存在activeEffect,则调用trackEffect将当前的activeEffect与ref.dep关联起来。triggerRefValue(ref):在set value()时调用。调用triggerEffects(ref.dep)遍历并执行所有依赖。
toRef 和 toRefs 实现:视图与数据的同步引用
这两个函数的作用是将响应式对象的一个属性提取出来,包装成一个引用对象,以便在解构或传递时保持其响应性。
核心结构:ObjectRefImp (toRef 的返回)
ObjectRefImp 是一个特殊的 Ref 对象,它没有自己的 dep,而是直接读写源对象的属性。
| 属性/方法 | 作用 | 响应性原理 |
|---|---|---|
_object |
源对象 | 存储传入的响应式对象(例如 reactive({a: 1}))。 |
_key |
属性名 | 存储要引用的键名(例如 'a')。 |
get value() |
代理读取 | 直接返回 this._object[this._key]。当读取时,实际上触发了源对象上的 get 陷阱,从而进行依赖收集。 |
set value(newValue) |
代理写入 | 直接设置 this._object[this._key] = newValue。当写入时,触发了源对象上的 set 陷阱,从而触发更新。 |
toRefs(object) 的作用
遍历传入对象(通常是一个
reactive对象)的所有可枚举属性。对每一个属性都调用
toRef,将其包装成一个ObjectRefImp实例。返回一个包含所有这些
Ref实例的新对象。用途: 允许我们解构响应式对象,同时不丢失其属性的响应性(在模板中仍然需要
.value,除非结合setup或proxyRefs)。
proxyRefs 实现:自动脱 Ref 逻辑
proxyRefs 主要用于在 setup 函数的返回值上,目的是在模板中或解构后,访问 ref 对象时可以省略 .value。
核心结构:Proxy 陷阱
| 陷阱 | 行为 | 分析 |
|---|---|---|
get(target, key, receiver) |
自动脱 Ref (Unwrap) | 1. 获取属性值 r。2. 检查 r 是否是 ref (r.__v_isRef)。3. 如果是 ref,返回 r.value4. 如果不是 ref,返回 r 本身。 |
set(target, key, value, receiver) |
自动更新 Ref | 1. 获取属性的旧值 oldValue。2. 如果 oldValue 是 ref,则将新值赋值给 oldValue.value(保持 ref 引用不变,只更新它的内部值)。3. 如果 oldValue 不是 ref,则执行默认的 Reflect.set 行为(修改对象上的属性)。 |
关键点
proxyRefs 实现了 Vue 3 Composition API 的一个重要特性:在 setup 函数的返回对象上使用展开运算符(...toRefs(state))后,模板中可以直接使用属性名而无需 .value。
Computed
通过结合 ReactiveEffect(依赖收集和触发的机制)和 RefImpl(值封装和外部追踪的机制)来实现了 懒惰求值(Lazy Evaluation) 和 结果缓存(Caching)。
Computed 的实现原理与核心流程总结
computed 的设计使其既是其依赖数据的消费者(当依赖变化时,它知道自己需要更新),又是外部 effect 的生产者(当它的值被访问时,它会被外部 effect 依赖)。
1. 核心结构:ComputedRefImpl 类
computed 函数返回一个 ComputedRefImpl 实例,这个实例承载了懒惰求值和缓存的所有逻辑。
| 组件 | 作用 | 对应方法 |
|---|---|---|
| 内部 Effect | 负责依赖追踪(Tracking)。它运行用户传入的 getter,并收集 getter 中所依赖的所有响应式数据(如 state.name)。 |
this.effect = new ReactiveEffect(...) |
_value |
缓存上一次计算的结果。 | N/A |
dirty 标志 |
用于判断是否需要重新计算。如果为 true(脏),则必须运行 getter。 |
this.effect.dirty / this._dirtyLevel |
| 外部 Ref 接口 | 负责被动追踪(Triggering)。它对外暴露 .value 接口,以便被外部的 effect 依赖。 |
get value() / set value(v) |
| 例子 |
1 | |
2. 懒惰求值与缓存(get value() 流程)
computed 的核心价值在于它只有在被访问时才执行计算,并且会缓存结果。
| 步骤 | 代码位置 | 描述 |
|---|---|---|
| A. 检查脏值 (Check Cache) | ComputedRefImpl.get value() |
首次访问或依赖项变更后,this.effect.dirty 为 true。如果 dirty 为 false,则直接返回上次缓存的 _value,跳过所有计算。 |
| B. 重新计算 (Run Getter) | this._value = this.effect.run() |
如果是脏值,则执行内部 effect.run(): 1. 清除旧依赖 (proCleanEffect)。 2. 运行用户 getter,重新收集所有依赖(依赖收集发生在 track 中)。 3. getter 的返回值被存储在 this._value 中(更新缓存)。 4. 内部 effect 的 _dirtyLevel 被重置为 0(NoDirty)。 |
| C. 外部依赖收集 (Track Consumer) | trackRefValue(this) |
如果当前有外部 effect 正在运行(即 activeEffect 存在),则将这个外部 effect 收集到 aliasName 的依赖中。这样,当 aliasName 变化时,外部 effect 会重新运行。 |
| D. 返回结果 | return this._value |
返回计算并缓存后的结果。 |
3. 响应式触发(Dependency Change 流程)
当计算属性所依赖的响应式数据(例如 state.name)发生变化时,会触发以下步骤:
| 步骤 | 代码位置 | 描述 |
|---|---|---|
| A. 触发依赖 | trigger(target, key, ...) |
state.name 发生改变,触发其对应的依赖(dep)集合。 |
| B. 标记为脏 | triggerEffects |
找到依赖 state.name 的 内部 aliasName.effect,并执行: effect2._dirtyLevel = 4 /* Dirty */。(将计算属性标记为脏,强制下次访问时重新计算) |
| C. 通知外部 Effect | effect2.scheduler() |
调用内部 effect 的 scheduler,即 triggerRefValue(this)。 这会找到所有外部依赖 aliasName 的 effect(在步骤 2.C 中收集的)。 |
| D. 外部 Effect 重新运行 | triggerEffects / effect.scheduler() |
外部 effect 被通知需要更新,调用其 scheduler(通常是 effect.run()),从而触发视图更新。 |
总结: 内部 effect 负责监听依赖,不执行用户回调,只负责标记自己为脏;外部 effect 负责读取 computed,并通过 dirty 标志和 run() 方法实现了 按需计算 的高效机制。
watch 和 watchEffect
核心结构与函数关系
watch:调用doWatch(source, cb, options),其中cb(回调函数)存在。watchEffect:调用doWatch(source, null, options),其中cb为null。doWatch:实现主要的侦听逻辑,创建并运行ReactiveEffect。
核心机制:doWatch
doWatch 函数是整个侦听系统的枢纽。它的主要任务是:
1. 规范化侦听源 (getter 的创建)
watch 可以侦听不同的数据源(ref、reactive 对象、getter 函数),doWatch 的第一步是将这些源统一包装成一个 getter 函数,供 ReactiveEffect 内部执行来收集依赖。
侦听源类型 (source) |
对应 getter |
依赖收集方式 |
|---|---|---|
isReactive(source) |
() => reactiveGetter(source) |
访问 reactive 对象时,会进入 traverse 遍历。 |
isRef(source) |
() => source.value |
访问 ref.value 时,触发 ref 的依赖收集。 |
isFunction(source) |
source (用户自定义的 getter) |
运行函数时,访问函数内部的响应式数据。 |
2. 深度遍历与依赖收集 (traverse)
作用: 强制访问一个响应式对象的所有嵌套属性,以实现 深度侦听 (
deep: true)。逻辑: 递归遍历对象的所有属性。
使用
seen(Set) 避免循环引用导致的无限递归。通过
depth和currentDepth控制遍历的深度(尽管在doWatch中,deep选项只控制是进行“深度” (undefined) 还是“浅层” (1) 遍历)。在遍历过程中访问属性 (
source[key]) 会触发Proxy的get陷阱,从而将当前的effect(通过activeEffect) 收集为该属性的依赖。
3. 创建更新任务 (job)
job 是数据变化时(或首次执行时)调用的核心更新逻辑。
watch(有cb) 逻辑:如果存在
clean函数(来自上一次回调),则调用它进行清理(例如取消异步请求)。调用
newValue = effect.run()重新执行 getter,重新收集依赖,并获取新值。调用用户回调函数
cb(newValue, oldValue, onCleanup),将新值、旧值和清理注册函数onCleanup传入。更新
oldValue = newValue。
watchEffect(无cb) 逻辑:- 直接调用
effect.run(),目的是重新执行副作用函数本身(即watchEffect传入的函数)。
- 直接调用
4. 清理机制 (onCleanup)
作用: 允许用户在回调函数内部注册一个副作用清理函数。
机制:
onCleanup(fn)将清理函数fn赋值给闭包中的clean变量。在下一次
job执行时,首先调用clean()。这对于处理异步操作的竞态条件非常重要。
5. ReactiveEffect 的创建与执行
创建:
const effect = new ReactiveEffect(getter, job)将规范化后的
getter作为fn(副作用)。将
job作为scheduler(调度器)。这意味着当依赖的数据变化时,不会直接执行getter,而是执行job。
初始化执行:
watch(immediate: true):直接调用job(),立即执行回调。watch(immediate: false):调用oldValue = effect.run()。这会执行一次getter来收集依赖和获取初始的oldValue,但不执行用户回调。watchEffect:调用effect.run(),执行副作用函数并收集依赖。
6. 返回值
doWatch返回一个unwatch函数,该函数调用effect.stop(),用于停止当前的响应式侦听。
好的,我来为您详细梳理一下您的虚拟 DOM 框架(由 runtime-core.js 和 runtime-dom.js 组成)的整个渲染和更新流程。
这个流程的核心思想是:将平台无关的逻辑(Diff 算法)和平台相关的 DOM 操作(Web API)分离。
Runtime-core
运行时(Runtime)核心流程梳理
整个运行时流程始于用户调用 render 函数,并结束于对 真实 DOM 的修改。
阶段一:初始化与渲染入口
这个阶段主要发生在 runtime-dom.js 中,用于创建渲染器并暴露给用户。
平台操作 API (
nodeOps/renderOptions) 定义 (在runtime-dom.js):定义了所有与浏览器 DOM 相关的操作,如
insert、remove、createElement、patchProp等。这些操作是 平台特有 的。
创建渲染器 (
createRenderer) (在runtime-core.js):runtime-core.js中的createRenderer(renderOptions)函数接收nodeOps作为参数。它返回一个包含核心
render逻辑的渲染器对象。
用户调用
render(在index.html):用户调用从
runtime-dom.js导入的render(vnode, container)函数。这是整个流程的起点,将 VNode 树和目标容器传入。
初始调度 (在
runtime-dom.js导出的render函数内部):检查
container._vnode(旧 VNode)是否存在。调用 核心
patch函数:patch(container._vnode || null, vnode, container)。
阶段二:核心 Patch (Diff) 逻辑
这是 runtime-core.js 的主要工作,负责比对新旧 VNode 并决定采取何种操作。
patch(n1, n2, container, anchor)
| VNode 状态 | 逻辑判断 | 下一步操作 |
|---|---|---|
| 初次挂载 | n1 === null |
调用 mountElement(n2, container)。 |
| VNode 相同 | n1 === n2 |
直接 return (无需操作)。 |
| VNode 类型不一致 | n1 && !isSameVnode(n1, n2) |
销毁旧节点 unmount(n1),然后将 n1 置为 null,转为初次挂载新节点。 |
| 处理 VNode 类型 | 根据 n2.type (Text, Fragment, 元素) 使用 switch 语句: |
|
| 1. Element | 调用 processElement(n1, n2, container)。 |
|
| 2. Text | 调用 processText(n1, n2, container)。 |
|
| 3. Fragment | 调用 processFragment(n1, n2, container)。 |
|
阶段三:具体操作执行
A. 挂载 (Mount) 流程(n1 === null 时):
mountElement(vnode, container)(在runtime-core.js):创建真实 DOM: 调用
hostCreateElement(vnode.type)(即document.createElement)。处理属性: 遍历
vnode.props,调用hostPatchProp(el, key, null, props[key])(传入旧值为null)。处理子节点 (
children):如果
shapeFlags为TEXT_CHILDREN(文本),调用hostSetElementText(el, vnode.children)。如果
shapeFlags为ARRAY_CHILDREN(数组),调用mountChildren(vnode.children, el)。
插入 DOM: 调用
hostInsert(el, container)(即container.insertBefore)。
mountChildren(children, container)(在runtime-core.js):- 递归遍历子节点数组,对每个子节点调用
patch(null, child, container),继续挂载。
- 递归遍历子节点数组,对每个子节点调用
B. 更新 (Patch) 流程(n1 !== null 时):
patchElement(n1, n2, container)(在runtime-dom.js):复用 DOM:
n2.el = n1.el,新旧 VNode 指向同一个真实 DOM。更新属性: 调用
patchProps(n1.props, n2.props, el)。更新子节点: 调用
patchChildren(n1, n2, el)。
patchProps(oldProps, newProps, el)(在runtime-dom.js):更新/添加属性: 遍历
newProps,调用patchProp(el, key, oldProps[key], newProps[key])。移除属性: 遍历
oldProps,如果属性不在newProps中,调用patchProp(el, key, oldProps[key], null)(传入新值为null)。
patchChildren(n1, n2, container)(在runtime-core.js或runtime-dom.js):这是最复杂的阶段,负责处理子节点数组的更新。
旧文本 vs 新数组: 清空旧文本,然后挂载新数组。
旧数组 vs 新文本: 卸载旧数组,设置新文本。
旧数组 vs 新数组: 著名的 Diff 算法 核心逻辑(头尾比对、最长递增子序列等)发生在这里,负责高效地对子节点进行移动、新增和删除。
总结图示
| 阶段 | 函数/模块 | 描述 | 关键操作 |
|---|---|---|---|
| 初始化 | createRenderer / render |
组装平台 API,暴露渲染接口。 | patch(n1, n2, container) |
| Diff 分发 | patch |
确定初次挂载还是更新;判断 VNode 类型是否一致。 | mountElement / patchElement / unmount |
| 元素更新 | patchElement |
复用真实 DOM (el)。 |
patchProps / patchChildren |
| 真实 DOM 操作 | host... / nodeOps |
在 mountElement 或 patchProps 内部调用浏览器 API。 |
createElement / setAttribute / insertBefore |
| 子节点更新 | patchChildren |
对子节点数组进行 Diff 算法比对,以最小化操作次数。 | 数组 Diff 逻辑 (高效定位、移动、删除) |
函数
一、 虚拟节点 (VNode) 与创建
虚拟 DOM(VNode)是描述真实 DOM 结构的 JavaScript 对象,是连接声明式 API 和命令式 DOM 操作的桥梁。
1. createVnode.ts
核心功能:创建 VNode 对象。
VNode 结构:
__v_isVnode: true:标记为 VNode。type:节点类型(如'div'、组件对象、Text或FragmentSymbol)。props:属性/事件。children:子节点(文本、数组或插槽)。key:用于 Diff 算法的键。el:对应的真实 DOM 节点(在渲染时关联)。shapeFlag:形状标识,用于快速判断 VNode 的类型和子节点类型(例如ELEMENT、STATEFUL_COMPONENT、ARRAY_CHILDREN、TEXT_CHILDREN、SLOTS_CHILDREN)。
辅助功能:
Text(Symbol('Text')) 和Fragment(Symbol("Fragment")):特殊类型的 VNode。isVnode(value):判断是否为 VNode。isSameVnode(n1, n2):判断两个 VNode 是否可以复用(type和key相同)。
2. h.ts
核心功能:
h函数(createElement的别名),用于创建 VNode。重载处理:根据参数个数和类型,灵活地将参数解析为
type、props和children,最终调用createVnode。2个参数:第二个参数可能是
props(对象但非数组)或children(VNode、数组或文本)。3个及以上参数:第三个参数或后续参数都视为
children。
二、 渲染器 (Renderer) 与 Diff 算法
渲染器实现了跨平台的 DOM 操作和 VNode 的挂载、更新与卸载。
1. renderer.ts
核心功能:通过
createRenderer(renderOptions)创建渲染器,实现平台无关性。关键方法:
patch(n1, n2, container, anchor):Diff 算法入口,对比新旧 VNode,根据 VNodetype和shapeFlag调用不同的process方法。mountElement:首次挂载元素 VNode,创建 DOM、设置属性、递归挂载子节点。patchElement:更新元素 VNode,复用 DOM (n2.el = n1.el),调用patchProps和patchChildren。unmount(vnode):卸载 VNode 对应的 DOM 元素。
子节点更新 (
patchChildren) 逻辑:处理四种核心情况:新文本 vs 旧数组:卸载旧数组,设置新文本。
新数组 vs 旧数组:调用
patchKeyedChildren进行 Diff。新空/数组 vs 旧文本:清空旧文本,如果新是数组则挂载新数组。
新文本 vs 旧文本:直接更新文本内容。
2. patchKeyedChildren (Diff 核心算法)
功能:对新旧 VNode 数组(
c1,c2)进行高效比对和更新,实现最小化 DOM 操作。流程:
头尾比对:从头部 (
i) 和尾部 (e1,e2) 同时向中间扫描,处理相同 VNode 并复用 (patch)。新增/删除:
若
i > e1(新多),则从i到e2依次插入新增 VNode。若
i > e2(旧多),则从i到e1依次卸载多余的旧 VNode。
乱序比对:处理中间不相同的部分。
映射表:建立新子节点的
key到索引的Map(keyToNewIndexMap)。查找与复用/删除:遍历旧子节点,通过
key查找新索引。找不到则删除 (unmount);找到则复用 (patch) 并记录映射关系到newIndexToOldIndex数组中。移动优化:计算
newIndexToOldIndex的最长递增子序列 (LIS) (getSquence)。移动操作:倒序遍历新子节点,如果节点索引不在 LIS 中,则进行
hostInsert移动操作;否则,保持不动(因为 LIS 中的元素顺序是正确的)。
3. seq.ts
核心功能:实现最长递增子序列 (LIS) 算法。
作用:在 Diff 算法中,用于找到无需移动的元素,从而指导
patchKeyedChildren执行最少的 DOM 移动操作。实现:使用贪心算法 + 二分查找来优化 LIS 的查找过程。
三、 组件化与调度
组件是逻辑复用的核心,而调度器确保了响应式更新的效率。
1. component.ts
核心功能:组件实例 (
instance) 的创建、初始化 (props,slots) 和设置。组件实例结构 (
instance):包含了组件的所有状态和上下文,如vnode、props、data、subTree、setupState、proxy等。setupComponent流程:initProps:初始化props(响应式)和attrs(非响应式)。initSlots:处理插槽children。instance.proxy:创建代理对象 (Proxy),用于在组件内部通过this访问data、props、setupState和公共属性(如$attrs、$slots)。执行
setup():获取setup返回值。如果返回函数,则作为render函数;如果返回对象,则通过proxyRefs进行脱ref处理,作为setupState。初始化
data():执行data函数并转换为响应式对象 (reactive)。
2. renderer.ts (组件相关)
processComponent:处理组件 VNode。挂载 (
mountComponent):创建组件实例 (createComponentInstance) -> 设置组件 (setupComponent) -> 创建组件更新effect(setupRenderEffect)。更新 (
updateComponent):通过shouldComponentUpdate判断是否需要更新。若需要,设置instance.next为新的 VNode,并调用instance.update()。
setupRenderEffect:为组件创建副作用 (ReactiveEffect)。componentUpdateFn:执行render函数获取subTree,然后对subTree进行patch。scheduler:将组件的更新函数 (update) 放入队列 (queueJob),实现异步批量更新。
3. scheduler.ts
核心功能:异步任务调度器,用于批量处理组件更新任务 (
job)。流程:
queueJob(job):将传入的job去重后放入queue数组。isFlushing:确保只开启一个异步任务。微任务:使用
Promise.resolve().then()开启一个微任务。批量执行:在微任务中,清空队列并依次执行所有缓存的
job,实现状态的异步批量更新。
4. apiLifecycle.ts (生命周期钩子)
apiLifecycle.ts 模块主要实现了 Vue 组件的生命周期钩子函数,允许用户在 setup 阶段注册回调函数,以便在组件生命周期的特定时刻执行逻辑。
核心机制
生命周期枚举 (
LifeCycle): 定义了各个生命周期阶段的短名称常量:BEFORE_MOUNT("bm")MOUNTED("m")BEFORE_UPDAATE("bu") (注意:文件中是BEFORE_UPDAATE,应为BEFORE_UPDATE)UPDATED("u")
createHook(type): 这是一个高阶函数,用于生成具体的生命周期钩子函数(如onMounted)。它接收一个生命周期类型 (
type)。返回一个钩子注册函数,该函数将用户提供的回调
hook注册到当前组件实例上(通过currentInstance获取)。
钩子注册逻辑:
钩子回调被存储在组件实例 (
target) 上的对应属性 (target[type]) 数组中。实际存储的是一个封装函数 (
wrapHook),这个封装函数的作用是:在执行用户提供的hook之前,通过setCurrentInstance(target)确保当前的hook是在正确的组件实例上下文中执行的,执行完毕后再通过unsetCurrentInstance()清空,以防止上下文污染。
导出的钩子函数:
onBeforeMountonMountedonBeforeUpdateonUpdated
执行工具 (
invokeArray):- 导出了一个工具函数
invokeArray(fns),用于遍历并执行一个函数数组,这是在渲染器中触发组件实例上注册的生命周期钩子时使用的。
- 导出了一个工具函数
5.apiProvide.ts (provide/inject)
apiProvide.ts 模块实现了依赖注入机制,允许父组件向其所有后代组件提供(provide)数据,后代组件则可以注入(inject)这些数据,实现跨级通信。
核心机制
provide(key, value) (提供数据)
上下文检查: 确保
provide是在组件的setup上下文中调用(即currentInstance存在)。继承机制:
组件实例 (
currentInstance) 上的provides属性存储了它提供的数据。它首先继承自父组件的
provides(currentInstance.parent?.provides)。
防止污染(原型链继承):
只有在组件第一次调用
provide时(即当前实例的provides与父实例的provides相同时),才会创建一个新的对象作为当前的provides。这个新对象是通过
Object.create(parentProvide)创建的,实现了原型继承。这意味着子组件可以读取父组件提供的数据,但子组件新增或覆盖数据时,不会影响父组件的原型链上的数据。
数据存储: 将
key和value存储到当前实例的provides对象上。
inject(key, defaultValue) (注入数据)
上下文检查: 确保在组件上下文中调用。
查找机制:
通过访问父组件的
provides属性 (currentInstance.parent?.provides) 开始查找。由于
provide实现了原型链继承,这个查找会沿着原型链向上(即沿着父组件、祖父组件…)查找,直到找到第一个匹配的key。
返回值:
如果父链的
provides中找到了key,则返回对应的值。否则,返回传入的
defaultValue。
Transition
Transition 组件旨在为元素的进入 (Enter) 和离开 (Leave) 提供基于 CSS 的过渡动画能力。
1. 核心架构
| 模块 | 职责 |
|---|---|
Transition 函数 |
这是用户使用的组件入口。它是一个封装器,将用户传入的 props (钩子和类名) 传递给核心实现组件。 |
BaseTranstionImple |
这是真正的过渡组件实现。它是一个抽象组件,不会渲染自身,但会将其唯一的子节点的 vnode.transition 属性设置为封装好的钩子,从而让渲染器 (Renderer) 在挂载/卸载时能够调用这些动画逻辑。 |
resolveTranstionProps |
核心逻辑封装层。 负责接收用户定义的类名和钩子,并将其转化为渲染器需要调用的 onBeforeEnter、onEnter、onLeave 等标准函数。 |
2. 过渡实现机制 (Enter / 进入动画)
进入过渡依赖于 三步曲 和 双帧延迟 (nextFram) 来确保浏览器正确感知到样式变化,从而触发 CSS 过渡。
| 步骤 | 钩子/代码位置 | 动作 | 目的 |
|---|---|---|---|
| 1. 准备阶段 | onBeforeEnter(el) |
1. 调用用户 onBeforeEnter。 2. 添加 *-enter-from 类 (起点样式)。 3. 添加 *-enter-active 类 (过渡属性,如 transition: opacity 3s)。 |
应用起点样式和过渡时间,准备就绪。 |
| 2. 动画触发 | onEnter(el, done) / nextFram |
1. 延迟一帧 (nextFram)。 2. 移除 *-enter-from 类。 3. 添加 *-enter-to 类 (终点样式)。 |
强制浏览器重绘,使浏览器感知到 from 到 to 的样式变化,触发 CSS 过渡。 |
| 3. 动画结束 | onEnter 内部的 resolve 函数 |
1. 移除 *-enter-to 类。 2. 移除 *-enter-active 类。 3. 移除 transitionend 监听器。 4. 调用 done() 回调。 |
清理 DOM 元素上的过渡类,将元素恢复到正常状态。 |
3. 离开实现机制 (Leave / 离开动画)
离开过渡用于元素在被卸载前播放动画,其核心是延迟卸载。
| 步骤 | 钩子/代码位置 | 动作 | 目的 |
|---|---|---|---|
| 1. 准备阶段 | onLeave(el, done) |
1. 添加 *-leave-from 类 (起点样式)。 2. 调用 document.body.offsetHeight。 3. 添加 *-leave-active 类 (过渡属性)。 |
强制浏览器同步重排,确保 from 样式被应用。 |
| 2. 动画触发 | onLeave(el, done) / nextFram |
1. 延迟一帧 (nextFram)。 2. 移除 *-leave-from 类。 3. 添加 *-leave-to 类 (终点样式)。 |
触发 from 到 to 的 CSS 过渡。 |
| 3. 动画结束 | onLeave 内部的 resolve 函数 |
1. 移除 *-leave-active 和 *-leave-to 类。 2. 移除 transitionend 监听器。 3. 调用 done() 回调 (performRemove )。 |
通知渲染器可以安全地从 DOM 中移除元素了。 |
4. 关键实现点和优化
nextFram: 使用双重requestAnimationFrame是为了确保浏览器在应用*-from样式后至少绘制一帧,再应用*-to样式,保证触发过渡。强制重排: 在
onLeave中使用document.body.offsetHeight(或类似属性) 是为了在添加*-leave-active之前,强制浏览器应用*-leave-from样式,以确保动画的起始状态正确设置。手动/自动模式: 代码通过检查用户是否传入
onEnter/onLeave的回调函数 (done) 来判断是进入自动过渡模式(组件自动监听transitionend)还是手动模式(用户需手动调用resolve)。transitionend清理: 确保在resolve函数中,通过el.removeEventListener("transitionend", resolve)移除了事件监听器,避免内存泄漏。与 Renderer 的协作: 渲染器 (
renderer.ts) 必须识别vnode.transition属性,并在unmount时调用transition.leave(el, performRemove),并将实际移除 DOM 的操作放在performRemove中,等待过渡组件调用done()。
### 异步组件定义器(defineAsyncComponent)
defineAsyncComponent 函数用于定义一个可以异步加载的组件,它允许组件在需要时才进行加载和渲染,从而实现代码分割(Code Splitting)和性能优化。
- 核心功能
延迟加载(Lazy Loading):接收一个
loader函数,该函数返回一个 Promise,用于异步获取实际的组件定义。状态管理:该函数返回的组件(
AsyncComponentImpl)内部管理着加载状态 (loading)、加载结果 (loaded) 和错误状态 (error),并使用响应式引用(ref)来驱动视图更新。生命周期控制:通过
setup钩子进行条件渲染,根据内部状态在三种模式间切换渲染:占位符/加载中组件:在加载期间显示。
错误组件:加载失败或超时时显示。
已加载组件:加载成功后显示实际组件。
- 可配置选项 (Options)
defineAsyncComponent 接收一个选项对象,用于定制异步加载的行为和用户体验:
| 选项名称 | 类型 | 默认值/用途 | 功能说明 |
|---|---|---|---|
loader |
Function |
必需 | 实际加载组件的工厂函数,必须返回一个 Promise。 |
loadingComponent |
Component |
null |
加载过程中显示的组件。 (示例中使用了 { template: '<h1>Loading...</h1>' }) |
errorComponent |
Component |
null |
加载失败或超时时显示的组件。 (示例中使用了 { template: '<h1>Error!</h1>' }) |
delay |
number |
200ms | 在显示 loadingComponent 之前等待的毫秒数。 (示例中设置为 0) |
timeout |
number |
30000ms | 加载组件的超时时间(毫秒)。超过此时间未加载完成将触发错误。 (示例中设置为 3500ms) |
suspensible |
boolean |
true |
是否支持 Suspense 组件 (如果存在)。 |
onError |
Function |
null |
错误处理回调函数。 |
- 错误处理与超时机制
文件内部实现了专门的逻辑来处理加载过程中的错误和超时:
延迟显示加载组件:使用
delayTimer延迟显示loadingComponent,避免在网络极快时闪烁加载状态。超时触发:如果加载时间超过
timeout设定的时间,将设置error.value = true,并抛出一个错误(例如示例中的"组件加载失败"错误)。错误捕获:
loader返回的 Promise 发生reject或超时触发,都会将错误赋值给error状态,并最终导致渲染errorComponent。
runtime-dom
提供一系列的domapi,
runtime-dom.js 是一个 Vue.js 运行时 (Runtime) 的核心部分代码,它将 虚拟 DOM (Virtual DOM, VNode) 概念与 浏览器 DOM 操作 结合起来,实现了基本的渲染和打补丁功能。
这段代码主要围绕 “如何将 VNode 渲染成真实的 DOM 元素” 这一核心目标展开,采用了 抽象化 和 模块化 的设计思路。
1. 跨平台抽象 (Renderer Abstraction)
Vue 的核心设计之一是将渲染逻辑与宿主环境(Host Environment,如浏览器 DOM、Canvas 或 Native)解耦。
createRenderer(renderOptions)(packages/runtime-core/src/renderer.ts):这是创建渲染器的 核心工厂函数。
它接收一个名为
renderOptions的对象。这个对象包含了所有宿主环境特有的操作(如
hostInsert、hostCreateElement、hostPatchProp等)。通过这种方式,渲染逻辑 (
patch、mountElement) 可以在不知道它是操作浏览器 DOM 还是其他环境的情况下工作。
2. 浏览器 DOM 实现 (Runtime-DOM Specifics)
packages/runtime-dom/src/index.ts 将核心渲染器与浏览器环境连接起来。
nodeOps(packages/runtime-dom/src/nodeOps.ts):这是一个包含所有 原生 DOM 操作 的对象。
它封装了
parentNode.insertBefore、el.removeChild、document.createElement等方法,是与浏览器 API 直接交互的低级接口。
patchProp(packages/runtime-dom/src/patchProp.ts):这是对元素 属性打补丁 (Patch) 的主入口函数。
它根据属性
key的不同,分派到不同的专业处理函数,实现 属性分类处理。处理分类:
class:patchClassstyle:patchStyle以
on开头且第三个字符不是小写字母的属性(即事件,如onClick),patchEvent其他属性
patchAttr
render(packages/runtime-dom/src/index.ts):这是用户调用的 最终渲染函数。
它将
patchProp和nodeOps封装成renderOptions,然后传递给createRenderer,生成并调用渲染器的render方法。
3. VNode 核心数据结构与创建
createVnode(packages/runtime-core/src/createVnode.ts):创建 虚拟节点 (VNode) 的函数。
VNode 结构: 包含
type、props、children、key、el和最重要的shapeFlag属性。shapeFlag: 一个位掩码(bitmask),用于 标识 VNode 的类型和子节点类型(如ELEMENT、TEXT_CHILDREN、ARRAY_CHILDREN等)。这在渲染和打补丁时用于快速判断 VNode 结构,提高性能。
h(packages/runtime-core/src/h.ts):用于创建 VNode 的辅助函数 (Hyperscript)。
它处理不同的参数数量(
arguments.length):2 个参数:可能是
(type, propsOrChildren)。3 个或更多参数:
h会将第三个及之后的参数视为 children 数组。
它主要负责将调用简化,并转发给
createVnode。
4. 关键打补丁逻辑 (Mounting & Patching)
patch(packages/runtime-core/src/renderer.ts):渲染器中的 核心递归函数,负责比较新旧 VNode (n1, n2) 并更新 DOM。
初始化挂载: 如果
n1为null(即第一次挂载),则调用mountElement。
mountElement(packages/runtime-core/src/renderer.ts):执行 元素首次挂载 的逻辑。
使用
hostCreateElement创建真实 DOM 元素。遍历
props并使用hostPatchProp应用属性。根据
shapeFlag处理子节点:如果是文本子节点 (TEXT_CHILDREN),使用hostSetElementText;如果是数组子节点 (ARRAY_CHILDREN),递归调用mountChildren。最后,使用
hostInsert将创建的 DOM 元素插入到容器中。
优化
PatchFlags优化
场景
Diff 算法无法避免新旧虚拟 DOM 中无用的比较, 通过 patchFlags标记Vnode动态部分, 可以跳过比对 那些 静态的, 不需要检查的部分
原理:编译器会在 VNode 对象上添加一个
patchFlag属性,这个属性就是通过位运算组合起来的标记,告诉运行时:只有
class属性是动态的。只有
style属性是动态的。只有它的子节点顺序可能发生了变化。
效果:渲染器(
renderer.ts)在打补丁时,可以根据patchFlag的值,精准地只更新 VNode 中发生变化的部分(dynamicChildren),而不是像 Vue 2 那样对所有属性进行递归比对,从而极大地提升了渲染和更新速度。
compiler-core
1. 总体流程概述
输入:模板字符串,例如
<div>{{ msg }} hello</div>输出:渲染函数代码,例如:
function render(_ctx) { return _createElementVNode("div", null, _createTextVNode(_toDisplayString(_ctx.msg) + " hello")) }核心步骤:
Parse:将模板解析成 AST(抽象语法树),表示模板的结构。
Transform:遍历 AST,进行优化和转换(如合并文本、添加 helper 调用)。
Codegen:基于转换后的 AST 生成 JS 代码(render 函数)。
文件映射:
AST 定义:fileciteturn0file0 (ast.ts)
Parser:fileciteturn0file2 (parser.ts)
Transform:fileciteturn0file4 (transform.ts)
Codegen & Compile:fileciteturn0file1 (index.ts 或 compile.ts)
Helpers:fileciteturn0file3 (runtimeHelper.ts)
入口函数在 compile 中:const ast = parse(template); transform(ast); return generate(ast); fileciteturn0file1
2. Parse 阶段:模板 → AST
目的:将字符串模板解析成树状结构(AST),方便后续处理。
关键概念:
上下文(Context):记录当前位置(line, column, offset)和源代码。函数如
createParserContext(template)创建它。fileciteturn0file2位置跟踪:使用
advanceBy、advancePositionMutation更新位置,支持错误定位。节点类型:从 fileciteturn0file0 导入
NodeTypes,如ROOT、ELEMENT、TEXT、INTERPOLATION等。
解析逻辑:
parseChildren:循环解析子节点,直到遇到结束标签或 EOF。
如果以
{{` 开头:`parseInterpolation` 处理插值({{ msg }}),创建INTERPOLATION节点,内部是SIMPLE_EXPRESSION`。如果以
<开头:parseElement处理元素,递归解析子节点。否则:
parseText处理纯文本。
parseTag:解析标签名、属性(
parseAttributes、parseAttribute),处理自闭合标签。parseAttributeValue:处理属性值,支持带引号和不带引号。
优化:解析后过滤空白文本节点,并用空格替换多余空白。
输出:根节点
ROOT,包含 children 数组。示例 AST:{ type: NodeTypes.ROOT, children: [ { type: NodeTypes.ELEMENT, tag: "div", children: [...] } ] }注意事项:
位置信息(loc):每个节点记录 start/end/source,便于调试。
未处理指令(如 v-if),当前只支持基本元素/文本/插值。
3. Transform 阶段:AST 优化与转换
目的:遍历 AST,进行语义转换、优化,并为 codegen 准备(如添加 codegenNode、记录 helpers)。
关键概念:
上下文(Context):
createTransformContext(root),包含helpers: Map<Symbol, number>用于记录运行时 helper(如TO_DISPLAY_STRING)。fileciteturn0file4遍历方式:先序 + 后序(使用 exits 数组存储后序回调)。
Helper 系统:通过
context.helper(Symbol)记录需要导入的运行时函数。最终挂到ast.helpers上。fileciteturn0file3
主要转换函数(按顺序注册在
transformNode数组中):transformExpression:处理插值表达式,如将
msg改为_ctx.msg。fileciteturn0file4transformText:合并相邻文本/插值成
COMPOUND_EXPRESSION(e.g., “hello” + → [‘hello’, ‘ + ‘, interpolation])。如果只有一个子节点,包装成TEXT_CALL(调用createCallExpression生成JS_CALL_EXPRESSIONfor createTextVNode)。fileciteturn0file4transformElement:将
ELEMENT节点转换为VNODE_CALL(使用createVnodeCall),设置 tag/props/children,并挂到根节点的codegenNode上。fileciteturn0file4 fileciteturn0file3
遍历过程:
traverseNode(node, context):执行每个 transform(如 transformElement 返回后序函数),然后递归子节点,最后执行后序回调。对于
INTERPOLATION,额外调用context.helper(TO_DISPLAY_STRING)。
输出:优化后的 AST,根节点有
codegenNode(VNODE_CALL),并有ast.helpers数组。注意事项:
后序处理确保子节点先转换(e.g., 文本合并后才生成 VNODE_CALL)。
当前未支持 v-for/v-if 等高级指令。
4. Codegen 阶段:AST → JS 代码
目的:生成可执行的 render 函数字符串。
关键概念:
- 上下文(Context):
createCodegenContext(ast),包含 code 缓冲、缩进管理、helper 映射(从 fileciteturn0file3 的helperMap取,如CREATE_ELEMENT_VNODE→ ‘_createElementVNode’)。
- 上下文(Context):
生成逻辑(
generate(ast)):genFunctionPreamble:生成导入语句,如
const { toDisplayString: _toDisplayString, ... } = Vue。基于ast.helpers。fileciteturn0file1主体:
return function render(_ctx) { return [genNode(ast.codegenNode)] }genNode:递归生成节点代码:
VNODE_CALL:genVNodeCall生成_createElementVNode(tag, props, children)。TEXT:JSON.stringify(content)。INTERPOLATION:genInterpolation生成_toDisplayString(_ctx.msg)。COMPOUND_EXPRESSION:拼接 children,如text + _toDisplayString(...)。JS_CALL_EXPRESSION:genCallExpression生成_createTextVNode(args)。TEXT_CALL:递归到其 codegenNode。
children 处理:如果是数组,用
[和]包裹;单个直接生成;null 则 ‘null’。
输出:字符串代码,可通过
new Function(code)()执行。注意事项:
当前 props 硬编码为 null,未解析属性。
支持基本优化,如文本合并减少 vnode 创建。
5. 常见问题与调试Tips
循环依赖:避免在 ast.ts 和 runtimeHelper.ts 间互导。统一在 runtimeHelper.ts 定义创建函数。fileciteturn0file0turn0file3
Helper 未定义:确保 transform 阶段记录到
ast.helpers,codegen 阶段用helperMap映射。文本合并Bug:检查
transformText的循环,确保从后往前遍历以避免索引错乱。测试:用简单模板测试全流程,如
<p>hi {{name}}</p>,检查生成的 render 是否正确。扩展:当前是 mini 版,可加 v-bind、v-on 等:parse 阶段加 DIRECTIVE,transform 加对应处理器。





