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)
<template>
<div>
<div class="title">show1</div>
<div>show1</div>
</div>
</template>
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 全静态
<template>
<div>
<div class="title" style="color: red" id="dom" title="show">
{{ message }}
</div>
<div>show1</div>
</div>
</template>
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)
触发条件:任何有动态绑定的节点
<template>
<div>
<div
:title="title"
:class="title"
:style="{ color: 'red', borderWidth: borderWidth }"
:id="dom"
:data-dom="dom"
>
show1
</div>
<div>show1</div>
</div>
</template>
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
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")的元素
【示例】
<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>
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
// 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)
}
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
}
事件缓存
【示例】
<div>
<button @click="console.log('xxx')">click</button>
<button @click="handleClick">点击</button>
<button @click="() => {}">点击</button>
</div>
未开启事件缓存 cacheHandlers 设置 false
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
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 */))
]))
}
【示例】
<div>
<button @click="handleClick(message)">click</button>
<button @click="handleClick('xx')">click</button>
</div>
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")
]))
}
【示例】
<div>
<input v-model="message" placeholder="请输入信息" />
</div>
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 |
【示例】
<div>
<p>段落</p>
<p v-if="tag === 1">这里是header</p>
<p v-else-if="tag === 2">这里是body</p>
<p v-else>这里是footer</p>
</div>
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"))
]))
}
【示例】
<div>
<ul>
<li v-for="item in tag" :key="item">信息{{ item }} end</li>
</ul>
</div>
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 函数执行:借助 openBlock 和 createBlock,将编译阶段标记出的动态节点,精准地收集到 dynamicChildren 数组中。
vue3-core/packages/runtime-core/src/vnode.ts
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
}
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 */,
),
)
}
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 */,
),
)
}