Vue 3.x 模板编译优化:静态提升、预字符串化与 Block Tree

Vue 3 在性能上的飞跃,很大程度上归功于编译时(compile-time)的深度优化。Vue 3 的编译器会尽可能多地分析模板,生成更高效的渲染函数代码。本文将从三个核心优化入手------静态提升 (含静态节点缓存、静态属性提升、动态属性列表提升)、预字符串化Block Tree

静态提升

静态节点缓存(CACHED /v-once)

  • 完全静态元素 → PatchFlags.CACHED 标记
  • 加入 toCache 列表 → 调用 context.cache()
  • 编译结果 :生成 _cache(0)运行时缓存
  • 不是提升到 render 外 (不是 _hoisted_1
js 复制代码
<template>
  <div>
    <div class="title">show1</div>
    <div>show1</div>
  </div>
</template>
js 复制代码
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
    _createElementVNode("div", { class: "title" }, "show1", -1 /* CACHED */),
    _createElementVNode("div", null, "show1", -1 /* CACHED */)
  ]))]))
}

静态属性提升(Hoisted Props)

触发条件 :节点动态(有文本 / 子节点更新),但 props 全静态

js 复制代码
<template>
  <div>
    <div class="title" style="color: red" id="dom" title="show">
      {{ message }}
    </div>
    <div>show1</div>
  </div>
</template>
js 复制代码
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = {
  class: "title",
  style: {"color":"red"},
  id: "dom",
  title: "show"
}

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("div", _hoisted_1, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _cache[0] || (_cache[0] = _createElementVNode("div", null, "show1", -1 /* CACHED */))
  ]))
}

动态属性列表提升(Hoisted dynamicProps)

触发条件:任何有动态绑定的节点

js 复制代码
<template>
  <div>
    <div
      :title="title"
      :class="title"
      :style="{ color: 'red', borderWidth: borderWidth }"
      :id="dom"
      :data-dom="dom"
    >
      show1
    </div>
    <div>show1</div>
  </div>
</template>
js 复制代码
import { normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = ["title", "id", "data-dom"]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("div", {
      title: _ctx.title,
      class: _normalizeClass(_ctx.title),
      style: _normalizeStyle({ color: 'red', borderWidth: _ctx.borderWidth }),
      id: _ctx.dom,
      "data-dom": _ctx.dom
    }, " show1 ", 14 /* CLASS, STYLE, PROPS */, _hoisted_1),
    _cache[0] || (_cache[0] = _createElementVNode("div", null, "show1", -1 /* CACHED */))
  ]))
}

第四个参数 patchFlag计算 = 2(class) + 4 (style) + 8(非class 、非style)

源码 walk

vue3-core/packages/compiler-core/src/transforms/cacheStatic.ts

js 复制代码
function walk(
  node: ParentNode,
  parent: ParentNode | undefined,
  context: TransformContext,
  doNotHoistNode: boolean = false,
  inFor = false,
) {
  const { children } = node // 获取子节点列表
  // 收集可缓存的静态节点(最终编译为 _cache 缓存)
  const toCache: (PlainElementNode | TextCallNode)[] = []

  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    // only plain elements & text calls are eligible for caching.

    // 一、普通元素节点
    // 只处理普通元素(非组件、非插槽等)
    if (
      child.type === NodeTypes.ELEMENT &&
      child.tagType === ElementTypes.ELEMENT
    ) {
      // 计算节点的常量类型
      const constantType = doNotHoistNode
        ? // 如果 doNotHoistNode 为 true(表示该节点不应被提升)
          ConstantTypes.NOT_CONSTANT
        : getConstantType(child, context)

      /**
          NOT_CONSTANT = 0, // 非常量表达式,在编译时无法确定其值
          CAN_SKIP_PATCH, // 1 可以跳过补丁的常量,通常是静态节点
          CAN_CACHE, // 2 可以缓存的常量,其值在编译时已知
          CAN_STRINGIFY, // 3 可以字符串化的常量,其值可以在编译时转换为字符串
         */
      // ============== 场景1:节点是静态节点 ==============
      if (constantType > ConstantTypes.NOT_CONSTANT) {
        if (constantType >= ConstantTypes.CAN_CACHE) {
          // 如果常量类型达到 CAN_CACHE(意味着节点极其稳定,可被 v-once 缓存)

          // 设置 patchFlag 为 PatchFlags.CACHED (即 -1,表示完全静态)
          ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED
          // 节点添加到 toCache 数组中
          toCache.push(child)
          continue
        }

        // ============== 场景2:节点非整体静态,但属性可提升 ==============
      } else {
        // node may contain dynamic children, but its props may be eligible for
        // hoisting.
        // 获取节点的生成节点
        const codegenNode = child.codegenNode!

        // 节点是VNODE_CALL 类型(虚拟节点调用)
        if (codegenNode.type === NodeTypes.VNODE_CALL) {
          const flag = codegenNode.patchFlag

          if (
            (flag === undefined || // 未标记的节点
              flag === PatchFlags.NEED_PATCH || // 需要比对的节点
              flag === PatchFlags.TEXT) && // 只有文本内容会变化的节点
            // 检查节点属性的常量类型
            getGeneratedPropsConstantType(child, context) >=
              ConstantTypes.CAN_CACHE
          ) {
            // 获取节点的属性对象
            const props = getNodeProps(child)
            if (props) {
              // 将属性对象提升到渲染函数外部
              // 更新代码生成节点
              codegenNode.props = context.hoist(props)
            }
          }
          // 动态属性列表(dynamicProps):它是编译器生成的一个静态字符串数组,仅用于记录哪些属性名是动态绑定的。
          // 这个数组本身不依赖任何响应式数据,所以可以被提升。
          if (codegenNode.dynamicProps) {
            codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
          }
        }
      }

      // 处理文本调用节点
    } else if (child.type === NodeTypes.TEXT_CALL) {
      const constantType = doNotHoistNode
        ? ConstantTypes.NOT_CONSTANT
        : // 计算节点的常量类型
          getConstantType(child, context)

      // 纯静态文本节点 → 加入缓存,避免重复生成文本 VNode。
      if (constantType >= ConstantTypes.CAN_CACHE) {
        if (
          // 代码生成节点类型为 JavaScript 调用表达式
          child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
          child.codegenNode.arguments.length > 0 // 参数大于0
        ) {
          child.codegenNode.arguments.push(
            // 添加 PatchFlags.CACHED 标记到参数列表中
            PatchFlags.CACHED +
              (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
          )
        }
        toCache.push(child) // 存储可缓存的节点
        continue
      }
    }

    // walk further
    // 递归处理元素节点
    // 表示元素节点,包括普通 HTML 元素和组件
    if (child.type === NodeTypes.ELEMENT) {
      const isComponent = child.tagType === ElementTypes.COMPONENT

      if (isComponent) {
        // 跟踪当前 v-slot 作用域的深度
        // 进入组件时增加计数
        context.scopes.vSlot++
      }
      walk(child, node, context, false, inFor)

      if (isComponent) {
        // 退出时减少计数
        context.scopes.vSlot--
      }

      // 递归处理 v-for 循环节点
    } else if (child.type === NodeTypes.FOR) {
      // Do not hoist v-for single child because it has to be a block
      walk(
        child,
        node,
        context,
        // 只有一个子节点,如果是则禁止提升
        // 原因:v-for 的单个子节点必须是一个块(block),因为 v-for 指令需要在 DOM 中创建和管理多个元素
        child.children.length === 1,
        true,
      )

      // 递归处理 v-if 条件判断节点
    } else if (child.type === NodeTypes.IF) {
      // 遍历 v-if 节点的所有分支,包括 if、else-if 和 else
      for (let i = 0; i < child.branches.length; i++) {
        // Do not hoist v-if single child because it has to be a block
        walk(
          child.branches[i],
          node,
          context,
          // 只有一个子节点,禁止提升
          // 原因:v-if 的单个子节点必须是一个块(block),因为 v-if 指令需要在 DOM 中创建和销毁元素
          child.branches[i].children.length === 1,
          inFor,
        )
      }
    }
  }

  let cachedAsArray = false // 缓存标识

  // 所有子节点都可缓存 并且 当前节点是元素节点
  if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
    if (
      node.tagType === ElementTypes.ELEMENT && // 普通 HTML 元素
      node.codegenNode &&
      node.codegenNode.type === NodeTypes.VNODE_CALL && // 代码生成节点是虚拟节点调用
      isArray(node.codegenNode.children) // 节点是数组形式
    ) {
      // all children were hoisted - the entire children array is cacheable.
      // 对整个子节点数组进行缓存
      node.codegenNode.children = getCacheExpression(
        createArrayExpression(node.codegenNode.children),
      )
      cachedAsArray = true // 标记为已缓存
    } else if (
      node.tagType === ElementTypes.COMPONENT && // 组件
      node.codegenNode &&
      node.codegenNode.type === NodeTypes.VNODE_CALL && // 代码生成节点是虚拟节点调用
      node.codegenNode.children && // 子节点存在
      !isArray(node.codegenNode.children) && // 子节点不是数组形式
      // 子节点是 JavaScript 对象表达式
      node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
    ) {
      // default slot
      // 获取名称为 'default' 的默认插槽
      const slot = getSlotNode(node.codegenNode, 'default')
      if (slot) {
        slot.returns = getCacheExpression(
          createArrayExpression(slot.returns as TemplateChildNode[]),
        )
        cachedAsArray = true // 标记已缓存
      }
    } else if (
      node.tagType === ElementTypes.TEMPLATE && // 模板
      parent &&
      parent.type === NodeTypes.ELEMENT && // 父节点是元素节点
      parent.tagType === ElementTypes.COMPONENT && // 父节点是组件
      parent.codegenNode &&
      parent.codegenNode.type === NodeTypes.VNODE_CALL && // 代码生成节点是虚拟节点调用
      parent.codegenNode.children && // 子节点存在
      !isArray(parent.codegenNode.children) && // 子节点不是数组形式
      // 子节点是 JavaScript 对象表达式
      parent.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
    ) {
      // named <template> slot
      // 获取名称为 'slot' 的插槽名称
      const slotName = findDir(node, 'slot', true)
      const slot =
        slotName &&
        slotName.arg &&
        getSlotNode(parent.codegenNode, slotName.arg)

      if (slot) {
        slot.returns = getCacheExpression(
          createArrayExpression(slot.returns as TemplateChildNode[]),
        )
        cachedAsArray = true // 标记已缓存
      }
    }
  }

  // 未标记缓存
  if (!cachedAsArray) {
    for (const child of toCache) {
      // 对每个子节点进行缓存
      child.codegenNode = context.cache(child.codegenNode!)
    }
  }

  /**
   * 缓存表达式
   * @param value 要缓存的表达式
   * @returns 缓存后的表达式
   */
  function getCacheExpression(value: JSChildNode): CacheExpression {
    // 创建缓存表达式,将传入的 value(通常是静态内容)进行缓存
    const exp = context.cache(value)
    // #6978, #7138, #7114
    // a cached children array inside v-for can caused HMR errors since
    // it might be mutated when mounting the first item
    // 问题:在 v-for 循环中使用缓存的子数组可能导致热模块替换(HMR)错误
    // 原因:当挂载第一个项目时,缓存的数组可能会被修改
    // 解决方法:通过数组展开避免直接修改原始缓存数组
    // #13221
    // fix memory leak in cached array:
    // cached vnodes get replaced by cloned ones during mountChildren,
    // which bind DOM elements. These DOM references persist after unmount,
    // preventing garbage collection. Array spread avoids mutating cached
    // array, preventing memory leaks.
    // 问题:缓存的 vnode 数组可能导致内存泄漏
    // 原因:
    // 1、在 mountChildren 期间,缓存的 vnode 会被克隆的 vnode 替换
    // 2、克隆的 vnode 会绑定 DOM 元素
    // 3、这些 DOM 引用在组件卸载后仍然存在,阻止垃圾回收
    // 解决方法:使用数组展开语法创建新数组,避免修改原始缓存数组,从而防止内存泄漏
    exp.needArraySpread = true // 设置数组展开标志
    return exp
  }

  /**
   * 获取插槽节点
   * @param node 生成代码节点
   * @param name 插槽名称
   * @returns 插槽节点
   */
  function getSlotNode(
    node: VNodeCall,
    name: string | ExpressionNode,
  ): SlotFunctionExpression | undefined {
    if (
      node.children && // 子节点存在
      !isArray(node.children) && // 子节点不是数组形式
      // 子节点是 JavaScript 对象表达式
      node.children.type === NodeTypes.JS_OBJECT_EXPRESSION
    ) {
      const slot = node.children.properties.find(
        // 属性的 key 直接等于 name
        // 属性的 key 是 SimpleExpressionNode 类型,且其 content 属性等于 name
        p => p.key === name || (p.key as SimpleExpressionNode).content === name,
      )
      // 返回其 value 属性(即插槽函数表达式)
      return slot && slot.value
    }
  }

  if (toCache.length && context.transformHoist) {
    // 静态提升
    context.transformHoist(children, context, node)
  }
}

PatchFlags 枚举

标志 (Flag) 数值 (Value) 含义说明
TEXT 1 元素文本内容是动态的(如 {{ msg }}
CLASS 1 << 1 = 2 元素的 class 绑定是动态的(如 :class="active"
STYLE 1 << 2 = 4 元素的 style 绑定是动态的(如 :style="{ color: red }"
PROPS 1 << 3 = 8 元素除 class/style 外,有其他动态属性(如 :id="userId"
FULL_PROPS 1 << 4 = 16 属性键(key)本身是动态的(如 :[propName]="value"),需要全量对比属性
HYDRATE_EVENTS 1 << 5 = 32 元素绑定了事件监听器(如 @click="handle"),主要用于服务端渲染后的"注水"(hydration)阶段
STABLE_FRAGMENT 1 << 6 = 64 片段(Fragment)的子节点顺序稳定,不会改变
KEYED_FRAGMENT 1 << 7 = 128 片段有带 key 的子节点,用于优化 v-for 列表渲染
UNKEYED_FRAGMENT 1 << 8 = 256 片段有无 key 的子节点,更新性能较差
NEED_PATCH 1 << 9 = 512 节点需要进行非属性(non-props)的补丁操作,如对 ref 或指令的处理
DYNAMIC_SLOTS 1 << 10 = 1024 组件含有动态插槽内容
DEV_ROOT_FRAGMENT 1 << 11 = 2048 仅在开发模式下,用于标记根片段
标志 (Flag) 数值 (Value) 含义说明
HOISTED -1 节点是静态的,已被提升,完全不需要参与 diff 对比
BAIL -2 渲染器应退出优化模式,进行完整的 diff 对比

预字符串化(Pre-stringification

Vue3 预字符串化(Pre-stringification) 是编译器在编译时针对大量连续静态节点的深度优化,将其直接合并为一个 HTML 字符串,大幅减少虚拟 DOM(VNode)数量与运行时开销。

触发条件

  • 节点数(NODE_COUNT):连续 ≥ 20 个纯静态节点
  • 带绑定元素数(ELEMENT_WITH_BINDING_COUNT):连续 ≥ 5 个含静态绑定(如 class="xxx")的元素

【示例】

js 复制代码
<div>
    <p>段落1.</p>
    <p>段落2.</p>
    <p>段落3.</p>
    <p>段落4.</p>
    <p>段落5.</p>
    <p>段落6.</p>
    <p>段落7.</p>
    <p>段落8.</p>
    <p>段落9.</p>
    <p>段落10.</p>
    <p>段落11.</p>
</div>
js 复制代码
import { createElementVNode as _createElementVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
    _createStaticVNode("<p>段落1.</p><p>段落2.</p><p>段落3.</p><p>段落4.</p><p>段落5.</p><p>段落6.</p><p>段落7.</p><p>段落8.</p><p>段落9.</p><p>段落10.</p><p>段落11.</p>", 11)
  ]))]))
}

源码 stringifyStatic

js 复制代码
//  Vue3 预字符串化(Pre-stringification)
// 把连续的纯静态节点 → 直接编译成 HTML 字符串 → 运行时 innerHTML 插入,彻底跳过 VNode 创建、Diff、DOM 逐个生成流程
export const stringifyStatic: HoistTransform = (children, context, parent) => {
  // bail stringification for slot content
  // 插槽内容不做字符串化(插槽有作用域、动态性)
  if (context.scopes.vSlot > 0) {
    return
  }

  // 判断父节点是否已缓存
  const isParentCached =
    parent.type === NodeTypes.ELEMENT &&
    parent.codegenNode &&
    parent.codegenNode.type === NodeTypes.VNODE_CALL &&
    parent.codegenNode.children &&
    !isArray(parent.codegenNode.children) &&
    parent.codegenNode.children.type === NodeTypes.JS_CACHE_EXPRESSION

  let nc = 0 // current node count 当前连续静态节点总数量
  let ec = 0 // current element with binding count 当前带绑定的静态元素数量
  const currentChunk: StringifiableNode[] = [] // 待合并的静态节点队列

  // 执行合并
  const stringifyCurrentChunk = (currentIndex: number): number => {
    if (
      nc >= StringifyThresholds.NODE_COUNT || // 大于20
      ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT // 大于5
    ) {
      // combine all currently eligible nodes into a single static vnode call
      // 创建静态 VNode 调用
      const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
        JSON.stringify(
          currentChunk.map(node => stringifyNode(node, context)).join(''),
        ).replace(expReplaceRE, `" + $1 + "`),
        // the 2nd argument indicates the number of DOM nodes this static vnode
        // will insert / hydrate
        String(currentChunk.length),
      ])

      const deleteCount = currentChunk.length - 1

      // 父节点已缓存:直接替换 children
      if (isParentCached) {
        // if the parent is cached, then `children` is also the value of the
        // CacheExpression. Just replace the corresponding range in the cached
        // list with staticCall.
        children.splice(
          currentIndex - currentChunk.length,
          currentChunk.length,
          // @ts-expect-error
          staticCall,
        )

        // 父节点未缓存:用第一个节点承载,删除剩下节点
      } else {
        // replace the first node's hoisted expression with the static vnode call
        ;(currentChunk[0].codegenNode as CacheExpression).value = staticCall
        if (currentChunk.length > 1) {
          // remove merged nodes from children
          children.splice(currentIndex - currentChunk.length + 1, deleteCount)
          // also adjust index for the remaining cache items
          const cacheIndex = context.cached.indexOf(
            currentChunk[currentChunk.length - 1]
              .codegenNode as CacheExpression,
          )
          if (cacheIndex > -1) {
            for (let i = cacheIndex; i < context.cached.length; i++) {
              const c = context.cached[i]
              if (c) c.index -= deleteCount
            }
            context.cached.splice(cacheIndex - deleteCount + 1, deleteCount)
          }
        }
      }
      return deleteCount
    }
    return 0
  }

  // 遍历子节点 → 收集连续静态节点 → 达到阈值就合并成 HTML 字符串 → 替换原节点
  let i = 0
  for (; i < children.length; i++) {
    const child = children[i]
    const isCached = isParentCached || getCachedNode(child)
    if (isCached) {
      // presence of cached means child must be a stringifiable node
      const result = analyzeNode(child as StringifiableNode)
      if (result) {
        // node is stringifiable, record state
        nc += result[0]
        ec += result[1]
        currentChunk.push(child as StringifiableNode)
        continue
      }
    }
    // we only reach here if we ran into a node that is not stringifiable
    // check if currently analyzed nodes meet criteria for stringification.
    // adjust iteration index
    i -= stringifyCurrentChunk(i)
    // reset state
    nc = 0
    ec = 0
    currentChunk.length = 0
  }
  // in case the last node was also stringifiable
  // 处理最后可能剩下的连续静态节点
  stringifyCurrentChunk(i)
}
js 复制代码
function analyzeNode(node: StringifiableNode): [number, number] | false {
  // 非可字符串化标签直接返回 false
  if (node.type === NodeTypes.ELEMENT && isNonStringifiable(node.tag)) {
    return false
  }

  // v-once nodes should not be stringified
  //  如果节点有 v-once 指令,返回 false(v-once 节点不应被字符串化)
  if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
    return false
  }

  // 如果节点是文本调用节点,直接返回 [1, 0](1个节点,0个带绑定的元素)
  if (node.type === NodeTypes.TEXT_CALL) {
    // 第一个数字:节点总数
    // 第二个数字:带有绑定的元素数量
    return [1, 0]
  }

  let nc = 1 // node count 节点计数
  let ec = node.props.length > 0 ? 1 : 0 // element w/ binding count 带有绑定的元素计数
  let bailed = false // 标记是否放弃分析
  const bail = (): false => {
    bailed = true // 标记为放弃分析
    return false
  }

  // TODO: check for cases where using innerHTML will result in different
  // output compared to imperative node insertions.
  // probably only need to check for most common case
  // i.e. non-phrasing-content tags inside `<p>`
  // 分析元素节点是否可以安全地被字符串化
  function walk(node: ElementNode): boolean {
    // 特殊标签处理
    const isOptionTag = node.tag === 'option' && node.ns === Namespaces.HTML

    // 属性检查
    for (let i = 0; i < node.props.length; i++) {
      const p = node.props[i]
      // bail on non-attr bindings
      // 普通属性并且不可字符串化,调用 bail() 放弃分析
      if (
        p.type === NodeTypes.ATTRIBUTE &&
        !isStringifiableAttr(p.name, node.ns)
      ) {
        return bail()
      }
      if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
        // bail on non-attr bindings
        // 指令参数并且不可字符串化,调用 bail() 放弃分析
        if (
          p.arg &&
          (p.arg.type === NodeTypes.COMPOUND_EXPRESSION ||
            (p.arg.isStatic && !isStringifiableAttr(p.arg.content, node.ns)))
        ) {
          return bail()
        }

        // 指令表达式并且不可字符串化,调用 bail() 放弃分析
        if (
          p.exp &&
          (p.exp.type === NodeTypes.COMPOUND_EXPRESSION ||
            p.exp.constType < ConstantTypes.CAN_STRINGIFY)
        ) {
          return bail()
        }
        // <option :value="1"> cannot be safely stringified
        // 对于 <option> 标签的 :value 绑定,特殊处理(非静态表达式不可字符串化)
        if (
          isOptionTag &&
          isStaticArgOf(p.arg, 'value') &&
          p.exp &&
          !p.exp.isStatic
        ) {
          return bail()
        }
      }
    }
    // 子节点检查
    for (let i = 0; i < node.children.length; i++) {
      nc++
      const child = node.children[i]
      if (child.type === NodeTypes.ELEMENT) {
        if (child.props.length > 0) {
          ec++
        }
        // 递归检查子节点
        walk(child)
        if (bailed) {
          return false
        }
      }
    }
    return true
  }

  return walk(node) ? [nc, ec] : false
}

事件缓存

【示例】

js 复制代码
  <div>
    <button @click="console.log('xxx')">click</button>
    <button @click="handleClick">点击</button>
    <button @click="() => {}">点击</button>
  </div>

未开启事件缓存 cacheHandlers 设置 false

js 复制代码
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: $event => (console.log('xxx'))
    }, "click", 8 /* PROPS */, _hoisted_1),
    _createElementVNode("button", { onClick: _ctx.handleClick }, "点击", 8 /* PROPS */, _hoisted_2),
    _cache[0] || (_cache[0] = _createElementVNode("button", { onClick: () => {} }, "点击", -1 /* CACHED */))
  ]))
}

开启事件缓存 cacheHandlers 设置 true

js 复制代码
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = $event => (console.log('xxx')))
    }, "click"),
    _createElementVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
    }, "点击"),
    _cache[2] || (_cache[2] = _createElementVNode("button", { onClick: () => {} }, "点击", -1 /* CACHED */))
  ]))
}

【示例】

js 复制代码
  <div>
    <button @click="handleClick(message)">click</button>
    <button @click="handleClick('xx')">click</button>
  </div>
js 复制代码
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = $event => (_ctx.handleClick(_ctx.message)))
    }, "click"),
    _createElementVNode("button", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.handleClick('xx')))
    }, "click")
  ]))
}

【示例】

js 复制代码
  <div>
    <input v-model="message" placeholder="请输入信息" />
  </div>
js 复制代码
import { vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _withDirectives(_createElementVNode("input", {
      "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((_ctx.message) = $event)),
      placeholder: "请输入信息"
    }, null, 512 /* NEED_PATCH */), [
      [_vModelText, _ctx.message]
    ])
  ]))
}

缓存的事件处理函数 $event => ((_ctx.message) = $event) 在组件整个生命周期中引用保持不变,但它却总能正确地将最新的输入值赋给响应式变量 _ctx.message

原因? 缓存的函数并没有"捕获" _ctx.message 的值,而是每次执行时动态地通过 _ctx 对象去访问 message 属性。而 _ctx 本身是一个组件实例的上下文代理对象 ,它在组件的整个生命周期中保持同一个引用,但其内部的属性(如 message)会随着响应式状态的变化而自动更新。

Block Tree

Block Tree 的核心理念是 将动态节点从静态节点中剥离出来,扁平化收集。它不是一个独立的运行时树形数据结构,而是编译器在模板编译阶段对 VNode 的一种标记和组织策略。

节点类型 原因说明
组件根节点 整个组件渲染的入口,天然形成一个 Block
带有 v-if / v-else / v-else-if 的节点 这些指令会导致节点的存在与否发生结构性变化,因此每个分支都会被包裹在一个独立的 Block 中
带有 v-for 的节点 列表渲染的节点结构可能会因数据变化而重排序或增删,所以会形成一个独立的 Block 来管理其动态子节点
多根节点模板(Fragment) 当模板有多个根节点时,这些根节点会被一个 Fragment 包裹,该 Fragment 节点也会成为一个 Block

【示例】

js 复制代码
  <div>
    <p>段落</p>
    <p v-if="tag === 1">这里是header</p>
    <p v-else-if="tag === 2">这里是body</p>
    <p v-else>这里是footer</p>
  </div>
js 复制代码
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"

const _hoisted_1 = { key: 0 }
const _hoisted_2 = { key: 1 }
const _hoisted_3 = { key: 2 }

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _cache[0] || (_cache[0] = _createElementVNode("p", null, "段落", -1 /* CACHED */)),
    (_ctx.tag === 1)
      ? (_openBlock(), _createElementBlock("p", _hoisted_1, "这里是header"))
      : (_ctx.tag === 2)
        ? (_openBlock(), _createElementBlock("p", _hoisted_2, "这里是body"))
        : (_openBlock(), _createElementBlock("p", _hoisted_3, "这里是footer"))
  ]))
}

【示例】

js 复制代码
  <div>
    <ul>
      <li v-for="item in tag" :key="item">信息{{ item }} end</li>
    </ul>
  </div>
js 复制代码
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("ul", null, [
      (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.tag, (item) => {
        return (_openBlock(), _createElementBlock("li", { key: item }, "信息" + _toDisplayString(item) + " end", 1 /* TEXT */))
      }), 128 /* KEYED_FRAGMENT */))
    ])
  ]))
}

源码

render 函数执行:借助 openBlockcreateBlock,将编译阶段标记出的动态节点,精准地收集到 dynamicChildren 数组中。

vue3-core/packages/runtime-core/src/vnode.ts

js 复制代码
function setupBlock(vnode: VNode) {
  // save current block children on the block vnode
  // 保存动态子节点
  // 只有当 isBlockTreeEnabled > 0 时才启用跟踪
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  // close block
  closeBlock()
  // a block is always going to be patched, so track it as a child of its
  // parent block
  // 只有当 isBlockTreeEnabled > 0 时才启用跟踪
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}
js 复制代码
function createBlock(
  type: VNodeTypes | ClassComponent, // 虚拟节点的类型,可以是标签名、组件等
  props?: Record<string, any> | null, // 节点的属性对象
  children?: any, // 节点的子节点
  patchFlag?: number, // 补丁标志,用于优化更新过程
  dynamicProps?: string[], // 动态属性数组,指定哪些属性是动态的
): VNode {
  return setupBlock(
    // 创建一个基础虚拟节点
    createVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      // 表示这是一个 block 节点,用于跟踪动态子节点
      true /* isBlock: prevent a block from tracking itself */,
    ),
  )
}
js 复制代码
function createElementBlock(
  type: string | typeof Fragment, // 字符串或 Fragment 符号,表示元素的标签名或片段
  props?: Record<string, any> | null, // 可选的属性对象,包含元素的属性、事件等
  children?: any, // 选的子节点,可以是字符串、数字、VNode 数组等
  patchFlag?: number, // 可选的补丁标志,用于优化更新过程
  dynamicProps?: string[], // 可选的动态属性数组,指定哪些属性是动态的
  shapeFlag?: number, // 可选的形状标志,表示 VNode 的类型
): VNode {
  // 将基础 VNode 转换为块节点
  return setupBlock(
    // 创建基础 VNode,设置各种属性和标志
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */,
    ),
  )
}
相关推荐
We་ct2 小时前
HTML5 原生拖拽 API 基础原理与核心机制
前端·javascript·html·api·html5·浏览器·拖拽
是上好佳佳佳呀2 小时前
【前端(八)】CSS3 属性值笔记:渐变、自定义字体与字体图标
前端·笔记·css3
踩着两条虫2 小时前
VTJ:核心引擎
前端·低代码·ai编程
GISer_Jing3 小时前
AI时代前端开发者成长计划
前端·人工智能
方安乐3 小时前
网页设计:自动适配浏览器深色/浅色模式
前端·html5
qq_12084093713 小时前
Three.js 工程向:后处理性能预算与多 Pass 链路优化
前端·javascript
南棱笑笑生3 小时前
20260422给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Buildroot时使用mpg123播放mp3音频
前端·javascript·音视频·rockchip
小小码农Come on3 小时前
QPainter双缓冲区实现一个简单画图软件
linux·服务器·前端
nunumaymax3 小时前
【第三章-react 应用(基于 react 脚手架)】
前端·react.js·前端框架