用位运算标记 vnode 的类型和 children 类型

2025-10-21 10:39:39

上一节实现了基础的 createVNode

但测试时会发现,只创建出 typepropschildren 这些字段还不够。

如果缺少 shapeFlag,渲染流程会出现类似这样的警告:

[Vue warn]: Invalid VNode type: div (string)

而当我们给 vnode 补上 shapeFlag: 9 后,内容就可以正常渲染了。

那这个 shapeFlag 到底是什么?

shapeFlag 是什么

shapeFlag 可以理解成 vnode 的类型标记。

记录了两个重要信息:

  • 当前 vnode 自己是什么类型
  • 当前 vnode 的 children 是什么类型
const vnode = h('div', 'hello world')

比如这个 vnode:它包含两层信息:

  • type 是字符串 'div',说明它是普通 DOM 元素
  • children 是字符串 'hello world',说明它的子节点是文本

如果每次渲染时都重新判断 typeof vnode.typetypeof vnode.children,当然也能做。

Vue 会在创建 vnode 的时候就把这些信息编码到 shapeFlag 里,后面渲染时直接判断标记。

可以把它类比成身份证。

身份证号码不是一个普通随机数,不同位置本身就带有含义,比如地区、出生日期、顺序码。

shapeFlag 也是类似的思路:一个数字里可以同时存放多个标记。

ShapeFlags

Vue 里会用枚举来定义这些标记:

export enum ShapeFlags {
  // DOM 元素
  ELEMENT = 1,

  // 函数组件
  FUNCTIONAL_COMPONENT = 1 << 1,

  // 有状态组件
  STATEFUL_COMPONENT = 1 << 2,

  // 子节点是文本
  TEXT_CHILDREN = 1 << 3,

  // 子节点是数组
  ARRAY_CHILDREN = 1 << 4,

  // 子节点是插槽
  SLOTS_CHILDREN = 1 << 5,

  // Teleport
  TELEPORT = 1 << 6,

  // Suspense
  SUSPENSE = 1 << 7,

  // 组件应该被 keep-alive
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,

  // 组件已经被 keep-alive
  COMPONENT_KEPT_ALIVE = 1 << 9,

  // 组件类型
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

这里的 1 << n 是左移运算。

它会让每个标记占据不同的二进制位:

ShapeFlags.ELEMENT // 0001
ShapeFlags.TEXT_CHILDREN // 1000
ShapeFlags.ARRAY_CHILDREN // 10000

这样不同标记之间不会互相覆盖,就可以放进同一个数字里。

为什么 shapeFlag 是 9

const vnode = h('div', 'hello world')

它是一个普通元素,并且子节点是文本。

所以它需要同时具备两个标记:

ShapeFlags.ELEMENT // 1
ShapeFlags.TEXT_CHILDREN // 8

用二进制表示就是:

0001
1000

通过或运算 | 合并:

0001
1000
----
1001

1001 转成十进制就是 9

所以 shapeFlag: 9 的意思不是一个神秘数字,它表示:

  • 这是一个元素节点
  • 它的 children 是文本

创建 shapeFlag

createVNode 里,可以先根据 type 得到基础标记。

let shapeFlag = 0

if (typeof type === 'string') {
  shapeFlag = ShapeFlags.ELEMENT
}

然后再根据 children 的类型,把对应标记合并进去:

if (typeof children === 'string') {
  shapeFlag = shapeFlag | ShapeFlags.TEXT_CHILDREN
}
else if (Array.isArray(children)) {
  shapeFlag = shapeFlag | ShapeFlags.ARRAY_CHILDREN
}

最终放到 vnode 上:

function createVNode(type, props?, children?) {
  const vnode = {
    __v_isVNode: true,
    type,
    props,
    children,
    key: props?.key,
    el: null,
    shapeFlag: 0
  }

  if (typeof type === 'string') {
    vnode.shapeFlag |= ShapeFlags.ELEMENT
  }

  if (typeof children === 'string') {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
  }
  else if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
  }

  return vnode
}

|= 等于 vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN

判断 shapeFlag

有了 shapeFlag 后,渲染器就可以通过与运算 & 判断 vnode 是否包含某个标记。

if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
  console.log('这是一个 DOM 元素')
}

if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  console.log('子节点是文本')
}