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: 提供了 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 框架的工作流程可以概括为:

  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 实例上的三个属性:_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
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.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

核心结构与函数关系

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

  2. watchEffect:调用 doWatch(source, null, options),其中 cb 为 null。

  3. 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) 逻辑:

    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.js 和 runtime-dom.js 组成)的整个渲染和更新流程。

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


Runtime-core

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

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

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

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

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

    • 定义了所有与浏览器 DOM 相关的操作,如 insert、remove、createElement、patchProp 等。

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

  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):

      • 如果 shapeFlags 为 TEXT_CHILDREN (文本),调用 hostSetElementText(el, vnode.children)。

      • 如果 shapeFlags 为 ARRAY_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.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 或 Fragment Symbol)。

    • 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,根据 VNode type 和 shapeFlag 调用不同的 process 方法。

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

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

    • 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(新多),则从 i 到 e2 依次插入新增 VNode。

      • 若 i > e2(旧多),则从 i 到 e1 依次卸载多余的旧 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):包含了组件的所有状态和上下文,如 vnode、props、data、subTree、setupState、proxy 等。

  • setupComponent 流程:

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

    2. initSlots:处理插槽 children。

    3. instance.proxy:创建代理对象 (Proxy),用于在组件内部通过 this 访问 data、props、setupState 和公共属性(如 $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. 数据存储: 将 key 和 value 存储到当前实例的 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 核心逻辑封装层。 负责接收用户定义的类名和钩子,并将其转化为渲染器需要调用的 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)和性能优化。

  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 的对象。

    • 这个对象包含了所有宿主环境特有的操作(如 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 :patchClass

      • style: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")) }

  • 核心步骤:

    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

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

    • 节点类型:从 fileciteturn0file0 导入 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)。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_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 定义创建函数。fileciteturn0file0turn0file3

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

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

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

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