2026.01.07
shapeFlag -> Vue 的类型标识系统
在上一章中,我们已经成功实现了渲染功能,但其中使用了一个硬编码的 shapeFlag: 9。那么这个神秘的数字到底代表什么含义呢?让我们深入探讨一下 Vue 中的类型标识系统。
在上一章中,我们已经成功实现了渲染功能,但其中使用了一个硬编码的 shapeFlag: 9。那么这个神秘的数字到底代表什么含义呢?让我们深入探讨一下 Vue 中的类型标识系统。
回顾示例代码
让我们回顾一下之前的渲染代码:
import { renderOptions } from '../dist/vue.esm.js'
const renderer = createRenderer(renderOptions)
const vnode = h('div', {
onClick() {
console.log('click')
},
id: 'node',
a: '1'
}, 'hello world')
renderer.render(vnode, app)
这段代码创建出来的 VNode 结构如下:
const vnode = {
__v_isVNode: true,
type, // div
props,
children, // hello world
key: props?.key,
el: null, // 虚拟节点要挂载的元素
shapeFlag: 9
}
什么是 shapeFlag?
对于上面的 vnode,如果要正确设置 shapeFlag,我们需要考虑:
type是一个 DOM 元素(字符串'div')children是一个字符串('hello world')
因此 shapeFlag = 9。
你可以把 shapeFlag 理解为一个"身份证",就像身份证的前六位是区域码(能代表省市区)一样,shapeFlag 通过二进制位来标识节点的多种特征。
Vue 官方的 ShapeFlags 定义
export enum ShapeFlags {
// 表示 DOM 元素
ELEMENT = 1, // 0001
// 表示函数组件
FUNCTIONAL_COMPONENT = 1 << 1, // 0010 (2)
// 表示有状态组件(带有状态、生命周期等)
STATEFUL_COMPONENT = 1 << 2, // 0100 (4)
// 表示该节点的子节点是纯文本
TEXT_CHILDREN = 1 << 3, // 1000 (8)
// 表示该节点的子节点是数组形式(多个子节点)
ARRAY_CHILDREN = 1 << 4, // 10000 (16)
// 表示该节点的子节点是通过插槽(slots)传入的
SLOTS_CHILDREN = 1 << 5, // 100000 (32)
// 表示 Teleport 组件,用于将子节点传送到其他位置
TELEPORT = 1 << 6, // 1000000 (64)
// 表示 Suspense 组件,用于处理异步加载组件时显示备用内容
SUSPENSE = 1 << 7, // 10000000 (128)
// 表示该组件应当被 keep-alive(缓存)
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 100000000 (256)
// 表示该组件已经被 keep-alive(已缓存)
COMPONENT_KEPT_ALIVE = 1 << 9, // 1000000000 (512)
// 表示组件类型,有状态组件与无状态函数组件的组合
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
位运算基础
这里使用了位运算(左移运算符 <<):
FUNCTIONAL_COMPONENT = 1 << 1:1向左移动 1 位,表示 2¹ = 2STATEFUL_COMPONENT = 1 << 2:1向左移动 2 位,表示 2² = 4TEXT_CHILDREN = 1 << 3:1向左移动 3 位,表示 2³ = 8
每个标志占据二进制的不同位,这样就可以通过组合来表示多种特征。
如何使用 shapeFlag
1. 设置元素类型标识
let shapeFlag = 0
const vnode = {
__v_isVNode: true,
type: 'div',
children: 'hello world',
shapeFlag
}
// 判断类型并设置标识
if (typeof vnode.type === 'string') {
shapeFlag = ShapeFlags.ELEMENT
}
vnode.shapeFlag = shapeFlag
// 判断是否是元素节点
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 与运算(&)的规则:两个位都为 1 时结果为 1
// 0001 & 0001 = 0001
console.log('这是一个 DOM 元素')
}
2. 组合多个标识
当一个节点需要同时标识多个特征时(比如既是元素又有文本子节点),我们使用**或运算(|)**来组合:
let shapeFlag = 0
const vnode = {
__v_isVNode: true,
type: 'div',
children: 'hello world',
shapeFlag
}
// 设置元素类型
if (typeof vnode.type === 'string') {
shapeFlag = ShapeFlags.ELEMENT // 0001
}
// 设置子节点类型
if (typeof vnode.children === 'string') {
// 使用或运算组合标识
shapeFlag |= ShapeFlags.TEXT_CHILDREN
// 等价于: shapeFlag = shapeFlag | ShapeFlags.TEXT_CHILDREN
/**
* 或运算(|)的规则:只要有一个位为 1,结果就为 1
*
* shapeFlag: 0001
* ShapeFlags.TEXT_CHILDREN: 1000
* --------------------------------
* 结果: 1001 (十进制 9)
*/
}
vnode.shapeFlag = shapeFlag // 最终值为 1001 (9)
// 检查是否为元素节点
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
/**
* 与运算(&)的规则:两个位都为 1 时结果才为 1
*
* vnode.shapeFlag: 1001
* ShapeFlags.ELEMENT: 0001
* --------------------------------
* 结果: 0001 (真值,通过检查)
*/
console.log('这是一个 DOM 元素')
// 检查是否有文本子节点
if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
/**
* vnode.shapeFlag: 1001
* ShapeFlags.TEXT_CHILDREN: 1000
* --------------------------------
* 结果: 1000 (真值,通过检查)
*/
console.log('子节点是字符串')
}
}
// 检查是否有数组子节点
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
/**
* vnode.shapeFlag: 01001
* ShapeFlags.ARRAY_CHILDREN: 10000
* ----------------------------------
* 结果: 00000 (假值,不通过检查)
*/
// 不会执行到这里
}
通过这种方式,一个数字就可以同时表示多个特征!
实现 createVNode 函数
基于单一职责原则,我们将 createVNode 和 isVNode 拆分到独立的 vnode.ts 文件中:
import { isArray, isString, ShapeFlags } from '@vue/shared'
/**
* 判断是否为虚拟节点
* @param value 待检查的值
* @returns 是否为 VNode
*/
export function isVNode(value) {
return value?.__v_isVNode
}
/**
* 创建虚拟节点
* @param type 节点类型(标签名或组件)
* @param props 节点属性
* @param children 子节点
* @returns VNode 对象
*/
export function createVNode(type, props?, children?) {
let shapeFlag = 0
// 判断节点类型
if (isString(type)) {
shapeFlag = ShapeFlags.ELEMENT
}
// 判断子节点类型
if (isString(children)) {
shapeFlag |= ShapeFlags.TEXT_CHILDREN
}
else if (isArray(children)) {
shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}
const vnode = {
__v_isVNode: true,
type,
props,
children,
key: props?.key,
el: null, // 虚拟节点要挂载的真实 DOM 元素
shapeFlag
}
return vnode
}
项目结构
现在 runtime-core 的目录结构如下:
runtime-core/
├── package.json
└── src/
├── h.ts # h 函数入口
├── h_test.ts # 测试文件
├── index.ts # 导出入口
└── vnode.ts # VNode 创建与判断
总结
通过本章的学习,我们了解了:
- shapeFlag 的作用:使用一个数字来标识虚拟节点的多种特征
- 位运算的应用:
- 左移运算
<<:定义不同的标志位 - 或运算
|:组合多个标志 - 与运算
&:检查是否包含某个标志
- 左移运算
- 为什么 shapeFlag = 9:因为
ELEMENT (1)和TEXT_CHILDREN (8)的组合就是1001(二进制)= 9(十进制) - 代码组织:遵循单一职责原则,将相关功能拆分到独立文件中
这种设计非常高效,只用一个数字就能表示多种特征,避免了使用多个布尔值或对象属性,节省了内存并提高了判断效率。