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 << 11 向左移动 1 位,表示 2¹ = 2
  • STATEFUL_COMPONENT = 1 << 21 向左移动 2 位,表示 2² = 4
  • TEXT_CHILDREN = 1 << 31 向左移动 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 函数

基于单一职责原则,我们将 createVNodeisVNode 拆分到独立的 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 创建与判断

总结

通过本章的学习,我们了解了:

  1. shapeFlag 的作用:使用一个数字来标识虚拟节点的多种特征
  2. 位运算的应用
    • 左移运算 <<:定义不同的标志位
    • 或运算 |:组合多个标志
    • 与运算 &:检查是否包含某个标志
  3. 为什么 shapeFlag = 9:因为 ELEMENT (1)TEXT_CHILDREN (8) 的组合就是 1001(二进制)= 9(十进制)
  4. 代码组织:遵循单一职责原则,将相关功能拆分到独立文件中

这种设计非常高效,只用一个数字就能表示多种特征,避免了使用多个布尔值或对象属性,节省了内存并提高了判断效率。