主题
实现虚拟 DOM - VNode
本节对标 Vue 3 源码
@vue/runtime-core中的vnode.ts
什么是 VNode?
VNode(Virtual Node)是真实 DOM 节点的 JavaScript 对象描述。Vue 3 通过 VNode 树来描述整个 UI 结构,然后通过渲染器将 VNode 转换为真实 DOM。
Template / Render Function → VNode Tree → Real DOM为什么需要 VNode?
- 跨平台:VNode 只是普通 JS 对象,不绑定任何平台 API
- Diff 优化:新旧 VNode 对比,只更新变化的部分
- 声明式编程:描述"UI 应该是什么样",而非"如何操作 DOM"
VNode 的类型
Vue 3 中 VNode 有以下几种类型:
ts
// 使用 const enum 定义 VNode 的形状标志,编译时会内联为数字,零运行时开销
// 每个标志占一个 bit 位,因此可以通过位运算组合和判断多种类型
export const enum ShapeFlags {
ELEMENT = 1, // 0b00000001 — 普通 DOM 元素(如 div、span)
FUNCTIONAL_COMPONENT = 1 << 1, // 0b00000010 — 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 0b00000100 — 有状态组件(包含 setup/data 等)
TEXT_CHILDREN = 1 << 3, // 0b00001000 — 子节点是纯文本
ARRAY_CHILDREN = 1 << 4, // 0b00010000 — 子节点是 VNode 数组
SLOTS_CHILDREN = 1 << 5, // 0b00100000 — 子节点是插槽函数
TELEPORT = 1 << 6, // 0b01000000 — Teleport 组件
SUSPENSE = 1 << 7, // 0b10000000 — Suspense 组件
// 组合标志:组件 = 有状态组件 | 函数式组件,方便统一判断是否为组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}位运算的巧妙设计
Vue 3 使用位运算来判断 VNode 类型,这比字符串比较或多个布尔值高效得多:
ts
// 判断是否是元素
if (vnode.shapeFlag & ShapeFlags.ELEMENT) { ... }
// 判断是否是组件
if (vnode.shapeFlag & ShapeFlags.COMPONENT) { ... }
// 判断子节点类型
if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) { ... }
// 组合标记
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN一个 VNode 可以同时具有多个标记(比如"是元素"且"有数组子节点")。
VNode 接口定义
ts
// VNode 接口定义 —— 虚拟 DOM 节点的完整数据结构
export interface VNode {
__v_isVNode: true // 标识位,用于快速判断一个对象是否为 VNode
type: VNodeTypes // 节点类型:可以是标签名字符串、组件对象或内置 Symbol
props: VNodeProps | null // 节点属性(class、style、事件等),null 表示无属性
children: VNodeNormalizedChildren // 标准化后的子节点
key: string | number | symbol | null // 用于 Diff 算法中唯一标识同级节点,优化复用
shapeFlag: number // 位运算标志位,编码了节点类型和子节点类型
el: any // 对应的真实 DOM 节点,mount 后会赋值,用于后续更新和卸载
component: any // 如果是组件 VNode,指向组件实例,方便访问组件的状态和方法
anchor: any // Fragment 的结束锚点,用于定位 Fragment 子节点的插入位置
}
// VNode 的 type 字段可以是以下几种类型
type VNodeTypes =
| string // 'div', 'span' — 原生 HTML 元素
| object // 组件选项对象(包含 setup、render 等)
| typeof Text // Symbol('Text') — 纯文本节点
| typeof Fragment // Symbol('Fragment') — 无根节点的片段
| typeof Comment // Symbol('Comment') — HTML 注释节点
// 属性类型:键值对形式,key 为属性名,value 为属性值
type VNodeProps = Record<string, any>
// 子节点的标准化类型:只允许文本、VNode 数组或 null 三种形式
type VNodeNormalizedChildren =
| string
| VNode[]
| null实现 createVNode
ts
// 定义内置的特殊 VNode 类型,使用 Symbol 确保唯一性,不会与任何 HTML 标签冲突
export const Text = Symbol('Text') // 纯文本节点类型
export const Fragment = Symbol('Fragment') // Fragment 类型,允许多根节点
export const Comment = Symbol('Comment') // 注释节点类型
// 创建 VNode 的核心函数,所有虚拟节点都通过此函数生成
export function createVNode(
type: VNodeTypes, // 节点类型
props: VNodeProps | null = null, // 节点属性,默认为 null
children: unknown = null, // 子节点,默认为 null
): VNode {
// 根据 type 的 JS 类型推断 VNode 的 shapeFlag
// 字符串 → 原生 DOM 元素;对象 → 有状态组件;函数 → 函数式组件
const shapeFlag = typeof type === 'string'
? ShapeFlags.ELEMENT
: typeof type === 'object'
? ShapeFlags.STATEFUL_COMPONENT
: typeof type === 'function'
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
// 构造 VNode 对象,初始化所有字段
const vnode: VNode = {
__v_isVNode: true, // 标识位,标记这是一个 VNode 对象
type, // 保存节点类型
props, // 保存属性
children: null, // 先设为 null,后续由 normalizeChildren 处理
key: props?.key ?? null, // 从 props 中提取 key,用于 Diff 时的节点复用
shapeFlag, // 初始的形状标志,后续会叠加子节点类型标志
el: null, // 真实 DOM 引用,mount 阶段赋值
component: null, // 组件实例引用,组件 mount 阶段赋值
anchor: null, // Fragment 锚点,Fragment mount 阶段赋值
}
// 标准化子节点,同时将子节点类型信息编码到 shapeFlag 中
normalizeChildren(vnode, children)
return vnode
}
// 标准化子节点:将 children 转为统一格式,并在 shapeFlag 中记录子节点类型
function normalizeChildren(vnode: VNode, children: unknown) {
if (typeof children === 'string' || typeof children === 'number') {
// 文本或数字子节点:统一转为字符串存储
vnode.children = String(children)
// 用位或运算叠加 TEXT_CHILDREN 标志到 shapeFlag
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
} else if (Array.isArray(children)) {
// 数组子节点:直接存储 VNode 数组
vnode.children = children as VNode[]
// 叠加 ARRAY_CHILDREN 标志
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}
}normalizeChildren 的作用
VNode 的 children 可能有多种形式,需要标准化:
ts
// 文本子节点
h('div', null, 'hello')
// 数组子节点
h('div', null, [h('span'), h('p')])
// 数字也当作文本
h('div', null, 42)通过 normalizeChildren,将 children 的类型信息编码到 shapeFlag 中,后续 patch 时可以快速判断。
h 函数
h 是 createVNode 的简写,提供更友好的 API:
ts
// h 函数是 createVNode 的简写封装,提供更灵活的参数形式
export function h(
type: VNodeTypes, // 节点类型(标签名或组件对象)
propsOrChildren?: VNodeProps | unknown, // 第二个参数可能是 props 也可能是 children
children?: unknown, // 第三个参数为明确的 children
): VNode {
const l = arguments.length // 根据实际传入参数个数决定如何解析
if (l === 2) {
// 只传了两个参数,需要判断第二个参数是 props 还是 children
if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
// 是对象且不是数组 → 当作 props 处理
// h('div', { class: 'foo' })
return createVNode(type, propsOrChildren as VNodeProps)
} else {
// 是字符串、数字或数组 → 当作 children 处理,props 设为 null
// h('div', 'text') 或 h('div', [child1, child2])
return createVNode(type, null, propsOrChildren)
}
} else {
// 传了三个参数 → 明确的 type + props + children
return createVNode(type, propsOrChildren as VNodeProps, children)
}
}h 函数的多态调用
ts
// 只有类型
h('div')
// 类型 + 属性
h('div', { id: 'foo', class: 'bar' })
// 类型 + 子节点(文本)
h('div', 'hello')
// 类型 + 子节点(数组)
h('div', [h('span', 'child1'), h('span', 'child2')])
// 类型 + 属性 + 子节点
h('div', { class: 'container' }, [h('p', 'paragraph')])
// 组件
h(MyComponent, { msg: 'hello' })createTextVNode
ts
export function createTextVNode(text: string): VNode {
return createVNode(Text, null, text)
}isVNode
ts
export function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}isSameVNodeType —— Diff 的基础
ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}这是 Diff 算法的核心判断条件 —— 只有 type 和 key 都相同的 VNode 才能复用。
对比 React
| 维度 | Vue 3 VNode | React Element / Fiber |
|---|---|---|
| 数据结构 | 普通对象 + shapeFlag | Element → Fiber |
| 类型标记 | 位运算 ShapeFlags | tag(枚举) |
| 创建 API | h() / createVNode() | jsx() / createElement() |
| 复用判断 | type + key | type + key |
| 真实节点引用 | vnode.el | fiber.stateNode |
测试用例
ts
describe('vnode', () => {
// 测试创建元素类型的 VNode,验证各字段是否正确
it('should create element vnode', () => {
const vnode = createVNode('div', { id: 'foo' }, 'hello')
expect(vnode.type).toBe('div') // type 应为传入的标签名
expect(vnode.props).toEqual({ id: 'foo' }) // props 应保留原始属性
expect(vnode.children).toBe('hello') // 文本子节点应被正确存储
// shapeFlag 应同时包含 ELEMENT 和 TEXT_CHILDREN 两个标志(位或组合)
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
)
})
// 测试数组子节点的 shapeFlag 是否正确叠加了 ARRAY_CHILDREN
it('should create vnode with array children', () => {
const vnode = createVNode('div', null, [createVNode('span')])
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN, // 应为 ELEMENT + ARRAY_CHILDREN
)
})
// 测试 h 函数传入文本子节点时的标准化行为
it('should normalize text children', () => {
const vnode = h('div', 'hello')
expect(vnode.children).toBe('hello') // h 的第二个参数应被当作 children 处理
})
})本节小结
- VNode — 真实 DOM 的 JS 对象描述,是渲染器的核心数据结构
- ShapeFlags — 位运算标记 VNode 类型,高效且可组合
- h 函数 — createVNode 的简写,支持多态调用
- isSameVNodeType — Diff 算法的入口判断
下一节实现渲染器的 mount 流程。