Vue3源码

![思维导图](【Xmind思维导图】Vue源码 https://app.xmind.cn/share/m95a8gBK?xid=RItKgcRT)

框架

Vue 3 的框架结构相比 Vue 2 进行了彻底的重构,使其更加模块化、高性能和 Tree-shaking 友好。

Vue 3 的核心不再是一个庞大的单体结构,而是由一系列职责清晰的模块组成的,这种设计被称为 “Monorepo” 架构


Vue 3 的模块化框架结构

Vue 3 的框架主要由以下几个核心包组成,它们协同工作,构成了 Vue 的运行时环境:

1. 响应式系统 (@vue/reactivity)

  • 核心作用: 这是 Vue 3 最底层也是最核心的机制。它负责跟踪状态的变化并触发视图更新。

  • 技术基础: 使用 JavaScript Proxy 实现。

  • 主要 API: 提供了 reactiverefreadonlycomputedwatch 等函数,用于创建和管理响应式状态。

  • 优势: 它是完全独立的,可以被用于任何 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 框架的工作流程可以概括为:

  1. 编译期 (Build Time 或 Runtime): 模板 通过 @vue/compiler-* 编译成 VNode 渲染函数

  2. 挂载期 (Mount): 渲染函数 被执行,创建 VNode 树,然后 @vue/runtime-dom 将 VNode 树转化为真实的 DOM 元素并插入页面。

  3. 响应式更新期 (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
2
//用于记录我们的代理后的结果 , 可以复用
const reactiveMap = new WeakMap()
  • 作用: reactiveMap 用于存储 原始对象target)和其对应的 Proxy 实例

  • 用 WeakMap的原因 使用 WeakMap 而不是普通 Map 的关键在于它的弱引用特性。如果原始对象不再被其他地方引用,WeakMap 不会阻止垃圾回收(GC)机制回收该对象及其对应的 Proxy,这有效防止了内存泄漏。

  • 缓存目的: 确保同一个原始对象不会被重复代理,总是返回同一个 Proxy 实例。

2. 响应式标记 (ReactiveFlags)

1
2
3
enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive', // 基本上唯一
}
  • 作用: 定义一个特殊的、不常用的 Symbol 或字符串键 (__v_isReactive),用于标记一个对象是否已经是响应式代理。

  • 实现方式: 通过 Proxy 的 get 捕获器,在访问这个特殊键时返回 true

3. 核心代理句柄 (mutableHandlers)

当一个对象被代理后,任何对它的属性访问都会经过这里的 get 捕获器:

1
2
3
4
5
6
7
8
9
10
11
12
13
const mutableHandlers: ProxyHandler<any> = {
get(target, key, recevier) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
 return Reflect.get(target, key, recevier) //recevier是proxy实例
},
set(target, key, value, recevier) {
return true
//找到属性, 让effect重新执行
return Reflect.set(target, key,value, recevier)
}
}
  • 如果尝试读取 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 实例上的三个属性:_trackedIddeps_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]),调用 cleanDepEffecteffect 从这些 dep 中删除。 for (let i = effect._depsLength; i < effect.deps.length; ++i) { cleanDepEffect(effect.deps[i], effect) }
截断 effect.deps 数组的长度截断为本次实际收集的长度。 effect.deps.length = effect._depsLength

四、 依赖触发 (triggertrackEffects)

元素 作用
trigger(target, key, ...) set 拦截器中被调用。它通过 targetkey 查找到对应的 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 将当前的 activeEffectref.dep 关联起来。

  • triggerRefValue(ref):在 set value() 时调用。调用 triggerEffects(ref.dep) 遍历并执行所有依赖。


toReftoRefs 实现:视图与数据的同步引用

这两个函数的作用是将响应式对象的一个属性提取出来,包装成一个引用对象,以便在解构或传递时保持其响应性。

核心结构: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,除非结合 setupproxyRefs)。


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. 如果 oldValueref,则将新值赋值给 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const state = reactive({name: 'p'})

    const aliasName = computed( {
      get(oldValue) {
        return 'ok' + state.name
      },
    })

    effect(() => {
      console.log(aliasName.value, '+')
    })

    setTimeout(()=> {
      state.name = '000'
      aliasName.value = '----'
    }, 3000)

2. 懒惰求值与缓存(get value() 流程)

computed 的核心价值在于它只有在被访问时才执行计算,并且会缓存结果。

步骤 代码位置 描述
A. 检查脏值 (Check Cache) ComputedRefImpl.get value() 首次访问或依赖项变更后,this.effect.dirtytrue。如果 dirtyfalse,则直接返回上次缓存的 _value,跳过所有计算。
B. 重新计算 (Run Getter) this._value = this.effect.run() 如果是脏值,则执行内部 effect.run(): 1. 清除旧依赖 (proCleanEffect)。 2. 运行用户 getter重新收集所有依赖(依赖收集发生在 track 中)。 3. getter 的返回值被存储在 this._value 中(更新缓存)。 4. 内部 effect_dirtyLevel 被重置为 0NoDirty)。
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() 调用内部 effectscheduler,即 triggerRefValue(this)。 这会找到所有外部依赖 aliasNameeffect(在步骤 2.C 中收集的)。
D. 外部 Effect 重新运行 triggerEffects / effect.scheduler() 外部 effect 被通知需要更新,调用其 scheduler(通常是 effect.run()),从而触发视图更新。

总结: 内部 effect 负责监听依赖,不执行用户回调,只负责标记自己为脏;外部 effect 负责读取 computed,并通过 dirty 标志和 run() 方法实现了 按需计算 的高效机制。

watch 和 watchEffect

核心结构与函数关系

  1. watch:调用 doWatch(source, cb, options),其中 cb(回调函数)存在。

  2. watchEffect:调用 doWatch(source, null, options),其中 cbnull

  3. doWatch:实现主要的侦听逻辑,创建并运行 ReactiveEffect


核心机制:doWatch

doWatch 函数是整个侦听系统的枢纽。它的主要任务是:

1. 规范化侦听源 (getter 的创建)

watch 可以侦听不同的数据源(refreactive 对象、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) 避免循环引用导致的无限递归。

    • 通过 depthcurrentDepth 控制遍历的深度(尽管在 doWatch 中,deep 选项只控制是进行“深度” (undefined) 还是“浅层” (1) 遍历)。

    • 在遍历过程中访问属性 (source[key]) 会触发 Proxyget 陷阱,从而将当前的 effect (通过 activeEffect) 收集为该属性的依赖。

3. 创建更新任务 (job)

job 是数据变化时(或首次执行时)调用的核心更新逻辑。

  • watch (有 cb) 逻辑:

    1. 如果存在 clean 函数(来自上一次回调),则调用它进行清理(例如取消异步请求)。

    2. 调用 newValue = effect.run() 重新执行 getter,重新收集依赖,并获取新值。

    3. 调用用户回调函数 cb(newValue, oldValue, onCleanup),将新值、旧值和清理注册函数 onCleanup 传入。

    4. 更新 oldValue = newValue

  • watchEffect (无 cb) 逻辑:

    1. 直接调用 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.jsruntime-dom.js 组成)的整个渲染和更新流程。

这个流程的核心思想是:将平台无关的逻辑(Diff 算法)和平台相关的 DOM 操作(Web API)分离。


Runtime-core

运行时(Runtime)核心流程梳理

整个运行时流程始于用户调用 render 函数,并结束于对 真实 DOM 的修改。

阶段一:初始化与渲染入口

这个阶段主要发生在 runtime-dom.js 中,用于创建渲染器并暴露给用户。

  1. 平台操作 API (nodeOps / renderOptions) 定义 (在 runtime-dom.js):

    • 定义了所有与浏览器 DOM 相关的操作,如 insertremovecreateElementpatchProp 等。

    • 这些操作是 平台特有 的。

  2. 创建渲染器 (createRenderer) (在 runtime-core.js):

    • runtime-core.js 中的 createRenderer(renderOptions) 函数接收 nodeOps 作为参数。

    • 它返回一个包含核心 render 逻辑的渲染器对象。

  3. 用户调用 render (在 index.html):

    • 用户调用从 runtime-dom.js 导入的 render(vnode, container) 函数。

    • 这是整个流程的起点,将 VNode 树和目标容器传入。

  4. 初始调度 (在 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 时):

  1. mountElement(vnode, container) (在 runtime-core.js):

    • 创建真实 DOM: 调用 hostCreateElement(vnode.type) (即 document.createElement)。

    • 处理属性: 遍历 vnode.props,调用 hostPatchProp(el, key, null, props[key]) (传入旧值为 null)。

    • 处理子节点 (children):

      • 如果 shapeFlagsTEXT_CHILDREN (文本),调用 hostSetElementText(el, vnode.children)

      • 如果 shapeFlagsARRAY_CHILDREN (数组),调用 mountChildren(vnode.children, el)

    • 插入 DOM: 调用 hostInsert(el, container) (即 container.insertBefore)。

  2. mountChildren(children, container) (在 runtime-core.js):

    • 递归遍历子节点数组,对每个子节点调用 patch(null, child, container),继续挂载。

B. 更新 (Patch) 流程(n1 !== null 时):

  1. patchElement(n1, n2, container) (在 runtime-dom.js):

    • 复用 DOM: n2.el = n1.el,新旧 VNode 指向同一个真实 DOM。

    • 更新属性: 调用 patchProps(n1.props, n2.props, el)

    • 更新子节点: 调用 patchChildren(n1, n2, el)

  2. patchProps(oldProps, newProps, el) (在 runtime-dom.js):

    • 更新/添加属性: 遍历 newProps,调用 patchProp(el, key, oldProps[key], newProps[key])

    • 移除属性: 遍历 oldProps,如果属性不在 newProps 中,调用 patchProp(el, key, oldProps[key], null) (传入新值为 null)。

  3. patchChildren(n1, n2, container) (在 runtime-core.jsruntime-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 mountElementpatchProps 内部调用浏览器 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'、组件对象、TextFragment Symbol)。

    • props:属性/事件。

    • children:子节点(文本、数组或插槽)。

    • key:用于 Diff 算法的键。

    • el:对应的真实 DOM 节点(在渲染时关联)。

    • shapeFlag形状标识,用于快速判断 VNode 的类型和子节点类型(例如 ELEMENTSTATEFUL_COMPONENTARRAY_CHILDRENTEXT_CHILDRENSLOTS_CHILDREN)。

  • 辅助功能

    • Text (Symbol('Text')) 和 Fragment (Symbol("Fragment")):特殊类型的 VNode。

    • isVnode(value):判断是否为 VNode。

    • isSameVnode(n1, n2):判断两个 VNode 是否可以复用(typekey 相同)。

2. h.ts
  • 核心功能h 函数(createElement 的别名),用于创建 VNode。

  • 重载处理:根据参数个数和类型,灵活地将参数解析为 typepropschildren,最终调用 createVnode

    • 2个参数:第二个参数可能是 props(对象但非数组)或 children(VNode、数组或文本)。

    • 3个及以上参数:第三个参数或后续参数都视为 children


二、 渲染器 (Renderer) 与 Diff 算法

渲染器实现了跨平台的 DOM 操作和 VNode 的挂载、更新与卸载。

1. renderer.ts
  • 核心功能:通过 createRenderer(renderOptions) 创建渲染器,实现平台无关性。

  • 关键方法

    • patch(n1, n2, container, anchor):Diff 算法入口,对比新旧 VNode,根据 VNode typeshapeFlag 调用不同的 process 方法。

    • mountElement:首次挂载元素 VNode,创建 DOM、设置属性、递归挂载子节点。

    • patchElement:更新元素 VNode,复用 DOM (n2.el = n1.el),调用 patchPropspatchChildren

    • unmount(vnode):卸载 VNode 对应的 DOM 元素。

  • 子节点更新 (patchChildren) 逻辑:处理四种核心情况:

    1. 新文本 vs 旧数组:卸载旧数组,设置新文本。

    2. 新数组 vs 旧数组:调用 patchKeyedChildren 进行 Diff。

    3. 新空/数组 vs 旧文本:清空旧文本,如果新是数组则挂载新数组。

    4. 新文本 vs 旧文本:直接更新文本内容。

2. patchKeyedChildren (Diff 核心算法)
  • 功能:对新旧 VNode 数组(c1, c2)进行高效比对和更新,实现最小化 DOM 操作

  • 流程

    1. 头尾比对:从头部 (i) 和尾部 (e1, e2) 同时向中间扫描,处理相同 VNode 并复用 (patch)。

    2. 新增/删除

      • i > e1(新多),则从 ie2 依次插入新增 VNode。

      • i > e2(旧多),则从 ie1 依次卸载多余的旧 VNode。

    3. 乱序比对:处理中间不相同的部分。

      • 映射表:建立新子节点的 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):包含了组件的所有状态和上下文,如 vnodepropsdatasubTreesetupStateproxy 等。

  • setupComponent 流程

    1. initProps:初始化 props(响应式)和 attrs(非响应式)。

    2. initSlots:处理插槽 children

    3. instance.proxy:创建代理对象 (Proxy),用于在组件内部通过 this 访问 datapropssetupState 和公共属性(如 $attrs$slots)。

    4. 执行 setup():获取 setup 返回值。如果返回函数,则作为 render 函数;如果返回对象,则通过 proxyRefs 进行脱 ref 处理,作为 setupState

    5. 初始化 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)。

  • 流程

    1. queueJob(job):将传入的 job 去重后放入 queue 数组。

    2. isFlushing:确保只开启一个异步任务。

    3. 微任务:使用 Promise.resolve().then() 开启一个微任务。

    4. 批量执行:在微任务中,清空队列并依次执行所有缓存的 job,实现状态的异步批量更新

4. apiLifecycle.ts (生命周期钩子)

apiLifecycle.ts 模块主要实现了 Vue 组件的生命周期钩子函数,允许用户在 setup 阶段注册回调函数,以便在组件生命周期的特定时刻执行逻辑。

核心机制
  1. 生命周期枚举 (LifeCycle): 定义了各个生命周期阶段的短名称常量:

    • BEFORE_MOUNT ("bm")

    • MOUNTED ("m")

    • BEFORE_UPDAATE ("bu") (注意:文件中是 BEFORE_UPDAATE,应为 BEFORE_UPDATE)

    • UPDATED ("u")

  2. createHook(type): 这是一个高阶函数,用于生成具体的生命周期钩子函数(如 onMounted)。

    • 它接收一个生命周期类型 (type)。

    • 返回一个钩子注册函数,该函数将用户提供的回调 hook 注册到当前组件实例上(通过 currentInstance 获取)。

  3. 钩子注册逻辑:

    • 钩子回调被存储在组件实例 (target) 上的对应属性 (target[type]) 数组中。

    • 实际存储的是一个封装函数 (wrapHook),这个封装函数的作用是:在执行用户提供的 hook 之前,通过 setCurrentInstance(target) 确保当前的 hook 是在正确的组件实例上下文中执行的,执行完毕后再通过 unsetCurrentInstance() 清空,以防止上下文污染。

  4. 导出的钩子函数:

    • onBeforeMount

    • onMounted

    • onBeforeUpdate

    • onUpdated

  5. 执行工具 (invokeArray):

    • 导出了一个工具函数 invokeArray(fns),用于遍历并执行一个函数数组,这是在渲染器中触发组件实例上注册的生命周期钩子时使用的。

5.apiProvide.ts (provide/inject)

apiProvide.ts 模块实现了依赖注入机制,允许父组件向其所有后代组件提供(provide)数据,后代组件则可以注入(inject)这些数据,实现跨级通信。

核心机制
provide(key, value) (提供数据)
  1. 上下文检查: 确保 provide 是在组件的 setup 上下文中调用(即 currentInstance 存在)。

  2. 继承机制:

    • 组件实例 (currentInstance) 上的 provides 属性存储了它提供的数据。

    • 它首先继承自父组件的 provides (currentInstance.parent?.provides)。

  3. 防止污染(原型链继承):

    • 只有在组件第一次调用 provide 时(即当前实例的 provides 与父实例的 provides 相同时),才会创建一个新的对象作为当前的 provides

    • 这个新对象是通过 Object.create(parentProvide) 创建的,实现了原型继承。这意味着子组件可以读取父组件提供的数据,但子组件新增或覆盖数据时,不会影响父组件的原型链上的数据。

  4. 数据存储:keyvalue 存储到当前实例的 provides 对象上。

inject(key, defaultValue) (注入数据)
  1. 上下文检查: 确保在组件上下文中调用。

  2. 查找机制:

    • 通过访问父组件provides 属性 (currentInstance.parent?.provides) 开始查找。

    • 由于 provide 实现了原型链继承,这个查找会沿着原型链向上(即沿着父组件、祖父组件…)查找,直到找到第一个匹配的 key

  3. 返回值:

    • 如果父链的 provides 中找到了 key,则返回对应的值。

    • 否则,返回传入的 defaultValue

Transition

Transition 组件旨在为元素的进入 (Enter)离开 (Leave) 提供基于 CSS 的过渡动画能力。

1. 核心架构
模块 职责
Transition 函数 这是用户使用的组件入口。它是一个封装器,将用户传入的 props (钩子和类名) 传递给核心实现组件。
BaseTranstionImple 这是真正的过渡组件实现。它是一个抽象组件,不会渲染自身,但会将其唯一的子节点的 vnode.transition 属性设置为封装好的钩子,从而让渲染器 (Renderer) 在挂载/卸载时能够调用这些动画逻辑。
resolveTranstionProps 核心逻辑封装层。 负责接收用户定义的类名和钩子,并将其转化为渲染器需要调用的 onBeforeEnteronEnteronLeave 等标准函数。
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 类 (终点样式)。 强制浏览器重绘,使浏览器感知到 fromto 的样式变化,触发 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 类 (终点样式)。 触发 fromto 的 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)和性能优化。

  1. 核心功能
  • 延迟加载(Lazy Loading):接收一个 loader 函数,该函数返回一个 Promise,用于异步获取实际的组件定义。

  • 状态管理:该函数返回的组件(AsyncComponentImpl)内部管理着加载状态 (loading)、加载结果 (loaded) 和错误状态 (error),并使用响应式引用(ref)来驱动视图更新。

  • 生命周期控制:通过 setup 钩子进行条件渲染,根据内部状态在三种模式间切换渲染:

    • 占位符/加载中组件:在加载期间显示。

    • 错误组件:加载失败或超时时显示。

    • 已加载组件:加载成功后显示实际组件。

  1. 可配置选项 (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 错误处理回调函数。
  1. 错误处理与超时机制

文件内部实现了专门的逻辑来处理加载过程中的错误和超时:

  • 延迟显示加载组件:使用 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 的对象。

    • 这个对象包含了所有宿主环境特有的操作(如 hostInserthostCreateElementhostPatchProp 等)。

    • 通过这种方式,渲染逻辑 (patchmountElement) 可以在不知道它是操作浏览器 DOM 还是其他环境的情况下工作。

2. 浏览器 DOM 实现 (Runtime-DOM Specifics)

packages/runtime-dom/src/index.ts 将核心渲染器与浏览器环境连接起来。

  • nodeOps (packages/runtime-dom/src/nodeOps.ts):

    • 这是一个包含所有 原生 DOM 操作 的对象。

    • 它封装了 parentNode.insertBeforeel.removeChilddocument.createElement 等方法,是与浏览器 API 直接交互的低级接口

  • patchProp (packages/runtime-dom/src/patchProp.ts):

    • 这是对元素 属性打补丁 (Patch)主入口函数。

    • 它根据属性 key 的不同,分派到不同的专业处理函数,实现 属性分类处理

    • 处理分类:

      • class :patchClass

      • style:patchStyle

      • on 开头且第三个字符不是小写字母的属性(即事件,如 onClick), patchEvent

      • 其他属性 patchAttr

  • render (packages/runtime-dom/src/index.ts):

    • 这是用户调用的 最终渲染函数

    • 它将 patchPropnodeOps 封装成 renderOptions,然后传递给 createRenderer,生成并调用渲染器的 render 方法。

3. VNode 核心数据结构与创建

  • createVnode (packages/runtime-core/src/createVnode.ts):

    • 创建 虚拟节点 (VNode) 的函数。

    • VNode 结构: 包含 typepropschildrenkeyel 和最重要的 shapeFlag 属性。

    • shapeFlag 一个位掩码(bitmask),用于 标识 VNode 的类型和子节点类型(如 ELEMENTTEXT_CHILDRENARRAY_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。

    • 初始化挂载: 如果 n1null(即第一次挂载),则调用 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")) }

  • 核心步骤

    1. Parse:将模板解析成 AST(抽象语法树),表示模板的结构。

    2. Transform:遍历 AST,进行优化和转换(如合并文本、添加 helper 调用)。

    3. Codegen:基于转换后的 AST 生成 JS 代码(render 函数)。

  • 文件映射

    • AST 定义:fileciteturn0file0 (ast.ts)

    • Parser:fileciteturn0file2 (parser.ts)

    • Transform:fileciteturn0file4 (transform.ts)

    • Codegen & Compile:fileciteturn0file1 (index.ts 或 compile.ts)

    • Helpers:fileciteturn0file3 (runtimeHelper.ts)

入口函数在 compile 中:const ast = parse(template); transform(ast); return generate(ast); fileciteturn0file1


2. Parse 阶段:模板 → AST

  • 目的:将字符串模板解析成树状结构(AST),方便后续处理。

  • 关键概念

    • 上下文(Context):记录当前位置(line, column, offset)和源代码。函数如 createParserContext(template) 创建它。fileciteturn0file2

    • 位置跟踪:使用 advanceByadvancePositionMutation 更新位置,支持错误定位。

    • 节点类型:从 fileciteturn0file0 导入 NodeTypes,如 ROOTELEMENTTEXTINTERPOLATION 等。

  • 解析逻辑

    • parseChildren:循环解析子节点,直到遇到结束标签或 EOF。

      • 如果以 {{` 开头:`parseInterpolation` 处理插值({{ msg }}),创建 INTERPOLATION节点,内部是SIMPLE_EXPRESSION`。

      • 如果以 < 开头:parseElement 处理元素,递归解析子节点。

      • 否则:parseText 处理纯文本。

    • parseTag:解析标签名、属性(parseAttributesparseAttribute),处理自闭合标签。

    • 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)。fileciteturn0file4

    • 遍历方式:先序 + 后序(使用 exits 数组存储后序回调)。

    • Helper 系统:通过 context.helper(Symbol) 记录需要导入的运行时函数。最终挂到 ast.helpers 上。fileciteturn0file3

  • 主要转换函数(按顺序注册在 transformNode 数组中):

    • transformExpression:处理插值表达式,如将 msg 改为 _ctx.msg。fileciteturn0file4

    • transformText:合并相邻文本/插值成 COMPOUND_EXPRESSION(e.g., “hello” + → [‘hello’, ‘ + ‘, interpolation])。如果只有一个子节点,包装成 TEXT_CALL(调用 createCallExpression 生成 JS_CALL_EXPRESSION for createTextVNode)。fileciteturn0file4

    • transformElement:将 ELEMENT 节点转换为 VNODE_CALL(使用 createVnodeCall),设置 tag/props/children,并挂到根节点的 codegenNode 上。fileciteturn0file4 fileciteturn0file3

  • 遍历过程

    • 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 映射(从 fileciteturn0file3 的 helperMap 取,如 CREATE_ELEMENT_VNODE → ‘_createElementVNode’)。
  • 生成逻辑generate(ast)):

    • genFunctionPreamble:生成导入语句,如 const { toDisplayString: _toDisplayString, ... } = Vue。基于 ast.helpers。fileciteturn0file1

    • 主体return function render(_ctx) { return [genNode(ast.codegenNode)] }

    • genNode:递归生成节点代码:

      • VNODE_CALLgenVNodeCall 生成 _createElementVNode(tag, props, children)

      • TEXTJSON.stringify(content)

      • INTERPOLATIONgenInterpolation 生成 _toDisplayString(_ctx.msg)

      • COMPOUND_EXPRESSION:拼接 children,如 text + _toDisplayString(...)

      • JS_CALL_EXPRESSIONgenCallExpression 生成 _createTextVNode(args)

      • TEXT_CALL:递归到其 codegenNode。

    • children 处理:如果是数组,用 [] 包裹;单个直接生成;null 则 ‘null’。

  • 输出:字符串代码,可通过 new Function(code)() 执行。

  • 注意事项

    • 当前 props 硬编码为 null,未解析属性。

    • 支持基本优化,如文本合并减少 vnode 创建。


5. 常见问题与调试Tips

  • 循环依赖:避免在 ast.ts 和 runtimeHelper.ts 间互导。统一在 runtimeHelper.ts 定义创建函数。fileciteturn0file0turn0file3

  • Helper 未定义:确保 transform 阶段记录到 ast.helpers,codegen 阶段用 helperMap 映射。

  • 文本合并Bug:检查 transformText 的循环,确保从后往前遍历以避免索引错乱。

  • 测试:用简单模板测试全流程,如 <p>hi {{name}}</p>,检查生成的 render 是否正确。

  • 扩展:当前是 mini 版,可加 v-bind、v-on 等:parse 阶段加 DIRECTIVE,transform 加对应处理器。