第 14 章

VNode 内部结构与 ShapeFlags 位运算

第14章:VNode 内部结构与 ShapeFlags 位运算

Vue 3 的 VNode 对象平均只有 12 个字段,其中 shapeFlag 一个整数就承担了原本需要 10 个 boolean 字段才能表达的类型信息——这不是炫技,而是 V8 引擎对小对象有专属的快速内联缓存路径。

本章核心问题:VNode 到底是什么结构?渲染器如何在微秒级别判断一个节点是组件还是原生元素还是文本?

读完本章你将理解

Level 1 · 你需要知道的(1-3年经验)

VNode 是什么:一个描述 UI 意图的普通 JS 对象

虚拟节点(VNode)不是 DOM,也不是组件实例。它是一张"施工图纸",描述了"我想要一个什么样的 DOM 节点或组件"。渲染器读取这张图纸,决定如何操作真实 DOM。

最重要的事实:一个 VNode 对象在内存中只活一帧。每次渲染,Vue 都会生成全新的 VNode 树,然后与上一帧的旧树对比(diff),只把差异应用到真实 DOM。

VNode 的 11 个核心字段

interface VNode {
  // 类型标识(最重要的字段)
  type: string | object | symbol | Function

  // diff 复用的唯一标识
  key: string | number | symbol | null

  // 模板 ref 引用
  ref: VNodeNormalizedRef | null

  // 传给元素或组件的属性对象
  props: Record<string, any> | null

  // 子节点:字符串、数组、插槽对象
  children: VNodeNormalizedChildren

  // 挂载后:指向组件实例(仅组件 VNode)
  component: ComponentInternalInstance | null

  // 挂载后:指向对应的真实 DOM 节点(仅元素 VNode)
  el: HostElement | null

  // 类型位掩码:用一个整数编码节点的所有类型特征
  shapeFlag: number

  // 编译器生成的更新提示(0 = 全量对比,>0 = 精确更新)
  patchFlag: number

  // 需要动态对比的 prop 名列表(仅在 patchFlag 包含 FULL_PROPS 时有效)
  dynamicProps: string[] | null

  // Block 树收集的动态子节点(跳过静态节点的关键)
  dynamicChildren: VNode[] | null
}

type 字段的四种形态

type 是渲染器判断如何处理这个 VNode 的第一依据:

type 的值 代表的节点类型 渲染器行为
'div''span''input' 等字符串 原生 HTML 元素 创建/更新 DOM 元素
{ setup, render, ... } 对象 有状态组件(选项式或 setup 式) 创建组件实例,调用 setup
(props, ctx) => VNode 函数 函数式组件 直接调用函数获取 VNode
Symbol(Fragment)Symbol(Text) 特殊容器节点 各自的特殊处理逻辑
// 字符串类型:原生元素
const divVNode = h('div', { class: 'box' }, 'Hello')
// divVNode.type === 'div'

// 对象类型:有状态组件
const MyComp = defineComponent({ setup() { return () => h('span', 'Hi') } })
const compVNode = h(MyComp, { title: 'test' })
// compVNode.type === MyComp

// Symbol 类型:Fragment(多根节点)
const fragVNode = h(Fragment, null, [h('li', 'a'), h('li', 'b')])
// fragVNode.type === Fragment(一个 Symbol)

三种特殊 Symbol 类型

// packages/runtime-core/src/vnode.ts
export const Fragment = Symbol('Fragment')
export const Text    = Symbol('Text')
export const Comment = Symbol('Comment')
export const Static  = Symbol('Static')

shapeFlag 字段的直觉理解

假设你要存一个"这个节点是不是有状态组件"的信息,最直观的方式是用一个 boolean:

vnode.isStatefulComponent = true
vnode.hasArrayChildren = false
vnode.hasSlotsChildren = true
// ... 再加 8 个 boolean

但 Vue 3 用一个数字表达同样的信息:

vnode.shapeFlag = 36  // 二进制 100100 = 有状态组件(4) | 插槽子节点(32)

这样做的好处:内存从 10 个字段减少到 1 个,类型检查从属性访问变成位运算(更快)。

日常开发中遇到 VNode 的场景

<script setup>
import { h, cloneVNode, useSlots } from 'vue'

const slots = useSlots()

// 访问插槽的 VNode
const defaultSlotVNodes = slots.default?.()

// 克隆并修改 VNode(用于高阶组件)
if (defaultSlotVNodes?.length) {
  const cloned = cloneVNode(defaultSlotVNodes[0], { class: 'extra-class' })
}
</script>

Level 2 · 它是怎么运行的(3-5年经验)

ShapeFlags 枚举完整定义

// packages/shared/src/shapeFlags.ts
export const enum ShapeFlags {
  ELEMENT                  = 1,        // 0000_0000_0001
  FUNCTIONAL_COMPONENT     = 1 << 1,   // 0000_0000_0010  = 2
  STATEFUL_COMPONENT       = 1 << 2,   // 0000_0000_0100  = 4
  TEXT_CHILDREN            = 1 << 3,   // 0000_0000_1000  = 8
  ARRAY_CHILDREN           = 1 << 4,   // 0000_0001_0000  = 16
  SLOTS_CHILDREN           = 1 << 5,   // 0000_0010_0000  = 32
  TELEPORT                 = 1 << 6,   // 0000_0100_0000  = 64
  SUSPENSE                 = 1 << 7,   // 0000_1000_0000  = 128
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 0001_0000_0000 = 256
  COMPONENT_KEPT_ALIVE     = 1 << 9,   // 0010_0000_0000  = 512
  COMPONENT                = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // = 6
}

注意 COMPONENT = 6,它是 STATEFUL_COMPONENT(4) | FUNCTIONAL_COMPONENT(2) 的按位或结果。这意味着:

// 一次位运算,同时检测两种组件类型
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
  // 无论有状态还是函数式,都走这里
}

位运算检测的内部实现

位运算原理图:

shapeFlag 值为 36 的 VNode(有状态组件 + 插槽子节点):

shapeFlag 的二进制表示:
  Bit 9  8  7  6  5  4  3  2  1  0
       0  0  0  0  1  0  0  1  0  0   = 36

检测 STATEFUL_COMPONENT (4 = 0000_0100):
  shapeFlag:  0000_0010_0100
  mask:       0000_0000_0100  (ShapeFlags.STATEFUL_COMPONENT)
  AND 结果:   0000_0000_0100  = 4(非零 → true,是有状态组件)

检测 ELEMENT (1 = 0000_0001):
  shapeFlag:  0000_0010_0100
  mask:       0000_0000_0001  (ShapeFlags.ELEMENT)
  AND 结果:   0000_0000_0000  = 0(零 → false,不是原生元素)

检测 SLOTS_CHILDREN (32 = 0010_0000):
  shapeFlag:  0000_0010_0100
  mask:       0000_0010_0000  (ShapeFlags.SLOTS_CHILDREN)
  AND 结果:   0000_0010_0000  = 32(非零 → true,有插槽子节点)

实际的渲染器 patch 分发代码:

// packages/runtime-core/src/renderer.ts (简化版)
function patch(n1, n2, container) {
  const { type, shapeFlag } = n2

  // 先按 type 分发特殊 Symbol 类型
  switch (type) {
    case Text:
      processText(n1, n2, container)
      break
    case Comment:
      processCommentNode(n1, n2, container)
      break
    case Fragment:
      processFragment(n1, n2, container)
      break
    default:
      // 再按 shapeFlag 分发普通类型
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        type.process(n1, n2, container, ...)
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        type.process(n1, n2, container, ...)
      }
  }
}

createVNode 的完整执行流程

createVNode() 执行链路:

createVNode(type, props, children)
    │
    ├── 1. 规范化 type(处理 defineComponent 返回值)
    │
    ├── 2. 计算 shapeFlag
    │       isString(type)          → ELEMENT(1)
    │       isArray(type)           → ELEMENT(1)(会被规范化)
    │       isSuspense(type)        → SUSPENSE(128)
    │       isTeleport(type)        → TELEPORT(64)
    │       isFunction(type)        → FUNCTIONAL_COMPONENT(2)
    │       isObject(type)          → STATEFUL_COMPONENT(4)
    │
    ├── 3. 规范化 props(处理 class/style 的特殊形式)
    │       class: ['a', {b: true}] → 'a b'(提前计算字符串)
    │       style: [{color:'red'}]  → {color: 'red'}(合并对象)
    │
    ├── 4. 规范化 children,更新 shapeFlag
    │       children 是字符串       → shapeFlag |= TEXT_CHILDREN(8)
    │       children 是数组         → shapeFlag |= ARRAY_CHILDREN(16)
    │       children 是对象(插槽) → shapeFlag |= SLOTS_CHILDREN(32)
    │
    ├── 5. 收集到当前 Block 的 dynamicChildren(若有 patchFlag)
    │
    └── 6. 返回 VNode 对象

createVNode vs createElementVNode vs h() 的差异

这三个函数是不同层级的抽象:

// h() 是用户层 API,做了最多的参数规范化
// 可以接受各种形式的参数
h('div', 'text')                    // children 是字符串
h('div', ['a', 'b'])                // children 是数组
h('div', { class: 'box' }, 'text')  // props + children

// createVNode() 是内部层,比 h() 少一层参数规范化
// 编译器不会生成 h(),而是生成 createVNode() 或更快的变体

// createElementVNode() 是 createVNode() 的特化版本
// 只处理原生元素(已知 type 是字符串),跳过 isObject/isFunction 检查
// 编译器对模板中的原生元素生成 createElementVNode(),比 createVNode() 快约 20%

// 实际编译结果:
// <div class="box">Hello</div>
// 编译为:
createElementVNode("div", { class: "box" }, "Hello")
// 而不是:
createVNode("div", { class: "box" }, "Hello")

cloneVNode 的使用场景与实现

cloneVNode 不是简单的对象浅拷贝,它需要处理 VNode 的多个字段的合并:

// packages/runtime-core/src/vnode.ts(简化)
export function cloneVNode(vnode, extraProps?, children?) {
  const { props, ref, patchFlag, children: existingChildren } = vnode

  const mergedProps = extraProps
    ? mergeProps(props || {}, extraProps)
    : props

  return {
    ...vnode,                          // 复制所有字段
    props: mergedProps,
    // ref 需要特殊处理:合并多个 ref
    ref: ...,
    // children 优先使用新传入的
    children: children ?? existingChildren,
    // patchFlag 重置为 -2(表示"已克隆,需要全量 diff")
    patchFlag: patchFlag === PatchFlags.HOISTED
      ? PatchFlags.HOISTED
      : patchFlag | PatchFlags.BAIL,
    // 克隆后的 VNode 不再属于原来的 Block
    dynamicChildren: null,
  }
}

常见使用场景:

// 场景1:高阶组件为插槽内容添加额外属性
const HOC = defineComponent({
  setup(_, { slots }) {
    return () => {
      const children = slots.default?.()
      if (!children) return null
      // 为所有插槽内容添加 class
      return children.map(vnode =>
        cloneVNode(vnode, { class: 'hoc-wrapped' })
      )
    }
  }
})

// 场景2:TransitionGroup 为子节点注入 key
// Vue 内部源码中大量使用 cloneVNode

Fragment VNode 的挂载与卸载

Fragment 的特殊性在于它没有对应的 DOM 节点,但需要知道自己的边界(用于后续的插入/移除操作):

Fragment 的 DOM 布局:

虚拟层:
  Fragment VNode
  ├── Text VNode "item A"
  ├── Text VNode "item B"
  └── Text VNode "item C"

真实 DOM 层:
  <!-- 起始锚点:存储在 vnode.el -->
  "item A"
  "item B"
  "item C"
  <!-- 结束锚点:存储在 vnode.anchor -->

卸载时:从 el 到 anchor 之间的所有 DOM 节点全部移除
// processFragment 的核心逻辑(简化)
function processFragment(n1, n2, container) {
  // 创建两个空文本节点作为锚点
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))

  if (n1 == null) {
    // 首次挂载:插入起点锚、挂载子节点、插入终点锚
    hostInsert(fragmentStartAnchor, container)
    mountChildren(n2.children, container, fragmentEndAnchor)
    hostInsert(fragmentEndAnchor, container)
  } else {
    // 更新:patch 子节点
    patchChildren(n1, n2, container, fragmentEndAnchor)
  }
}

组件 VNode vs 元素 VNode 的挂载差异

元素 VNode 挂载(mountElement):
  1. createElement(type)           → 创建真实 DOM 节点
  2. mountChildren(children)       → 递归处理子节点
  3. patchProps(el, props)         → 设置 class/style/事件等
  4. insert(el, container)         → 插入 DOM 树

组件 VNode 挂载(mountComponent):
  1. createComponentInstance()     → 创建组件实例对象
  2. setupComponent(instance)      → 执行 setup(),处理 props/slots
  3. setupRenderEffect(instance)   → 创建响应式副作用
     ├── 首次执行:调用 render() 得到 subTree VNode
     ├── patch(null, subTree)      → 递归挂载 subTree
     └── 后续:数据变化时重新执行

Level 3 · 设计文档与源码(资深开发者)

VNode 为什么不用 class 而用普通对象

这是一个关键的设计决策。Vue 2 的 VNode 是用 class 定义的:

// Vue 2 的方式
class VNode {
  constructor(tag, data, children, text, elm, ...) {
    this.tag = tag
    this.data = data
    // ... 20+ 个字段
  }
}

Vue 3 改为工厂函数返回普通对象:

// packages/runtime-core/src/vnode.ts
const vnode: VNode = {
  __v_isVNode: true,
  __v_skip: true,        // 告诉响应式系统不要追踪这个对象
  type,
  key,
  ref,
  // ...
}

原因有三:

  1. V8 的隐藏类(Hidden Class)优化:工厂函数每次返回具有相同字段顺序的对象,V8 可以将它们归入同一个隐藏类,触发内联缓存(Inline Cache)。class 实例在理论上也可以,但工厂函数更容易保证字段顺序稳定。
  2. __v_skip: true 标记:响应式系统(reactive())看到这个标记会直接跳过,不会把 VNode 变成响应式对象。这防止了大量不必要的追踪。
  3. Tree-shaking 友好:工厂函数可以被静态分析,class 方法较难被 tree-shaking。

patchFlag 的精确语义

// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
  TEXT          = 1,        // 动态文本内容
  CLASS         = 1 << 1,   // 动态 class 绑定
  STYLE         = 1 << 2,   // 动态 style 绑定
  PROPS         = 1 << 3,   // 动态 props(不含 class/style)
  FULL_PROPS    = 1 << 4,   // 包含动态 key 的 props,需要全量 diff
  HYDRATE_EVENTS = 1 << 5,  // SSR 水合时需要处理的事件监听器
  STABLE_FRAGMENT = 1 << 6, // 子节点顺序不变的 Fragment
  KEYED_FRAGMENT = 1 << 7,  // 有 key 的 Fragment(v-for)
  UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 Fragment(v-for without key)
  NEED_PATCH    = 1 << 9,   // 需要对比(如 ref、指令)
  DYNAMIC_SLOTS = 1 << 10,  // 动态插槽
  HOISTED       = -1,       // 静态提升的 VNode,永不更新
  BAIL          = -2,       // 退出优化模式,全量对比(cloneVNode 后使用)
}

编译器对 <div :class="cls">{{ text }}</div> 生成:

createElementVNode(
  "div",
  { class: _ctx.cls },  // 动态 class
  toDisplayString(_ctx.text),  // 动态文本
  3  // patchFlag = CLASS(2) | TEXT(1) = 3
)

更新时,patchElement 看到 patchFlag = 3

// packages/runtime-core/src/renderer.ts(简化)
function patchElement(n1, n2) {
  const { patchFlag, dynamicProps } = n2

  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // 只更新 class,不碰 style/其他 props
      hostPatchProp(el, 'class', null, n2.props!.class)
    }
    if (patchFlag & PatchFlags.TEXT) {
      // 只更新文本,直接 textContent 赋值
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children as string)
      }
    }
    // 跳过所有其他检查
  } else if (patchFlag === PatchFlags.BAIL || patchFlag === 0) {
    // 全量 diff:对比所有 props
    patchProps(el, n2, oldProps, newProps)
  }
}

VNode 对象的内存布局分析

一个典型的元素 VNode 对象占用内存估算:

VNode 内存布局(V8 64位环境):

字段                 类型         大小(估算)
─────────────────────────────────────────────
__v_isVNode         boolean      1 byte(对象头中标志位)
__v_skip            boolean      1 byte
type                pointer      8 bytes(指向字符串或对象)
key                 pointer      8 bytes
ref                 pointer      8 bytes
props               pointer      8 bytes(指向 props 对象)
children            pointer      8 bytes
component           null         8 bytes
el                  null         8 bytes
shapeFlag           int32        4 bytes  ← 位掩码的价值所在
patchFlag           int32        4 bytes
dynamicProps        null         8 bytes
dynamicChildren     null         8 bytes
─────────────────────────────────────────────
对象头 + 字段:       约 120-160 bytes

若改用 10 个独立 boolean 字段:
isElement/isComponent/.../  每个 boolean 在对象中占 8 bytes(指针槽)
额外增加: 10 × 8 = 80 bytes

源码中 shapeFlag 的设置链路

// packages/runtime-core/src/vnode.ts(关键代码路径)

export function createBaseVNode(type, props, children, ...) {
  const vnode: VNode = {
    // ...
    shapeFlag: isString(type)
      ? ShapeFlags.ELEMENT
      : __FEATURE_SUSPENSE__ && isSuspense(type)
        ? ShapeFlags.SUSPENSE
        : isTeleport(type)
          ? ShapeFlags.TELEPORT
          : isObject(type)
            ? ShapeFlags.STATEFUL_COMPONENT
            : isFunction(type)
              ? ShapeFlags.FUNCTIONAL_COMPONENT
              : 0,
    // ...
  }

  // children 规范化后更新 shapeFlag
  normalizeChildren(vnode, children)
  // normalizeChildren 内部:
  //   vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN  (若 children 是字符串)
  //   vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN (若 children 是数组)
  //   vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN (若 children 是对象)

  return vnode
}

为什么 COMPONENT = STATEFUL | FUNCTIONAL = 6

这个合并值使渲染器可以用一次位运算同时匹配两种组件,而不需要两次检查:

// 不好的写法(两次检查):
if (
  (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) ||
  (shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
) { ... }

// Vue 3 的实际写法(一次检查):
if (shapeFlag & ShapeFlags.COMPONENT) {
  // ShapeFlags.COMPONENT = 6 = 0110
  // STATEFUL(4) = 0100 → 0100 & 0110 = 0100 ≠ 0 → true
  // FUNCTIONAL(2) = 0010 → 0010 & 0110 = 0010 ≠ 0 → true
  // ELEMENT(1) = 0001 → 0001 & 0110 = 0000 = 0 → false
}

这是位掩码设计的精髓:精心选择的位分配可以让"分组检查"用一次操作完成。


Level 4 · 边界与陷阱(全体适用)

陷阱1:直接修改 VNode 对象的属性

// 危险:直接修改 VNode 会导致不可预测的行为
const vnode = h('div', { class: 'box' })
vnode.props.class = 'other'  // 永远不要这样做!

// 原因:
// 1. VNode 可能是静态提升的(HOISTED),所有渲染共享同一个对象
// 2. 直接修改会绕过响应式系统,下次更新时对比结果错乱
// 3. patchFlag 已经告诉渲染器哪些字段是静态的,直接修改打破这个约定

// 正确做法:使用 cloneVNode
const modified = cloneVNode(vnode, { class: 'other' })

陷阱2:在异步函数中使用 h() 构建 VNode 树

// 问题:异步环境中,currentBlock 可能指向错误的 Block
async function fetchAndRender() {
  const data = await fetch('/api/data')
  // ⚠️ 此处 await 之后,openBlock/createBlock 的上下文已丢失
  return h('div', data.json())
}

// 更安全的写法:在 setup() 同步阶段使用响应式数据
const data = ref(null)
onMounted(async () => {
  data.value = await fetch('/api/data').then(r => r.json())
})
// 模板中用 v-if="data" 处理异步状态

陷阱3:误解 key 的作用域

<template>
  <!-- 错误:两个不同的 v-for 使用相同的 key 值不会互相影响 -->
  <!-- key 只在同一个父节点的兄弟节点之间起作用 -->
  <div v-for="item in list1" :key="item.id">{{ item.name }}</div>
  <div v-for="item in list2" :key="item.id">{{ item.name }}</div>
  <!-- 上面两个 v-for 的 key 互不影响,即使值相同也没问题 -->

  <!-- 错误:以为给父元素加 key 能重置子组件 -->
  <div key="parent-key">
    <MyComponent />  <!-- key 在 div 上,MyComponent 不会重置 -->
  </div>

  <!-- 正确:要重置子组件,key 必须直接加在组件上 -->
  <MyComponent :key="someValue" />
</template>

陷阱4:静态提升后的 VNode 被 cloneVNode 破坏

// 编译器对静态节点做了提升(hoisting)
// _hoisted_1 在模块顶层只创建一次
const _hoisted_1 = createElementVNode("div", { class: "static-box" }, "永远不变")
// _hoisted_1.patchFlag === PatchFlags.HOISTED === -1

// 如果你用 cloneVNode 修改它:
const cloned = cloneVNode(_hoisted_1, { 'data-extra': 'value' })
// cloned.patchFlag 会变成 PatchFlags.BAIL(-2)
// 这意味着这个克隆版本每次更新都会做全量 diff
// 性能上不如直接在模板中声明动态属性

陷阱5:函数式组件的 shapeFlag 误判

// 陷阱:有时组件定义的写法决定了 shapeFlag
const Comp1 = { render() { return h('div') } }          // shapeFlag: STATEFUL_COMPONENT(4)
const Comp2 = (props) => h('div')                       // shapeFlag: FUNCTIONAL_COMPONENT(2)
const Comp3 = defineComponent({ render() { return h('div') } })  // shapeFlag: STATEFUL_COMPONENT(4)

// defineComponent 始终返回传入的对象(isObject → STATEFUL)
// 即使你传入了一个函数:
const Comp4 = defineComponent((props) => () => h('div'))
// setup 函数返回渲染函数,但 Comp4 本身是 defineComponent 包装后的对象
// shapeFlag 仍然是 STATEFUL_COMPONENT(4),不是 FUNCTIONAL_COMPONENT(2)

陷阱6:dynamicChildren 为 null 时的误判

// cloneVNode 后 dynamicChildren 被清空
const original = createElementVNode('div', null, 'text', PatchFlags.TEXT)
// original.dynamicChildren = [...] (由当前 Block 收集)

const cloned = cloneVNode(original)
// cloned.dynamicChildren = null  ← 清空了!
// cloned.patchFlag = original.patchFlag | PatchFlags.BAIL = -2

// 问题:如果你把 cloned 放入 openBlock/createBlock 的上下文中
// 它的 dynamicChildren 是 null,Block 优化不起作用
// 渲染器会退回到全量 diff 模式

本章小结

  1. VNode 是一帧的施工图:平均 11 个字段,type 决定渲染路径,shapeFlag 用位掩码编码类型信息,patchFlag 告诉渲染器哪些部分是动态的。

  2. ShapeFlag 位掩码的价值:一个 int32 字段替代了 10 个 boolean,节省了约 80 字节的对象体积,并让类型检查从属性访问变成了单次位运算(比 === 快 2-5 倍在高频路径中)。

  3. 三种创建函数的定位h() 是用户层(做最多规范化),createVNode() 是内部层,createElementVNode() 是编译器专用快速路径(跳过类型检查),理解这个层级有助于优化手写 render 函数的性能。

  4. Fragment 的 DOM 锚点机制:Fragment VNode 通过 el(起始锚点)和 anchor(结束锚点)两个空文本节点标记自己在 DOM 中的边界,这使得 Fragment 可以在不知道父节点的情况下被移除和移动。

  5. 永远不要直接修改 VNode:VNode 是不可变的施工图,静态提升的 VNode 在所有渲染周期共享,直接修改会破坏 patchFlag 约定并导致渲染器行为异常;需要修改时使用 cloneVNode()

本章评分
4.6  / 5  (21 评分)

💬 留言讨论