优化器的目标是遍历生成的模板 AST
(抽象语法树),检测纯静态的子树,即那些永远不需要改变的 DOM
部分。
当被标记成静态子树后,可以:
- 将它们提升为常量,这样在每次重新渲染时就不需要重新创建节点;
- 在更新(
patching
)过程中完全跳过这些静态节点。
这种优化对于提升 Vue
应用的性能很重要,特别是在有大量静态内容的页面中。通过跳过静态内容的更新检查,可以显著提高渲染性能。
1.核心函数
optimize
函数会优先遍历一遍所有节点,然后标记上静态节点。然后再遍历一遍节点,如果某节点下全部是静态节点,则该节点被标记为静态根节点。
核心函数逻辑如下:
js
export function optimize(root: ASTElement, options: CompilerOptions) {
// 1. 标记静态节点
markStatic(root)
// 2. 标记静态根节点
markStaticRoots(root, false)
}
2.静态节点
markStatic
函数用来标记静态节点。
- 判断节点是否是静态的;
- 递归处理子节点;
- 如果子节点不是静态的,父节点也标记为非静态。
js
function markStatic(node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// ...
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
// ...
}
// ...
}
其中的 isStatic
函数判断规则如下:
js
function isStatic(node: ASTNode): boolean {
// 表达式节点永远不是静态的
if (node.type === 2) return false
// 纯文本节点是静态的
if (node.type === 3) return true
// 其他情况需要满足:
return !!(
node.pre || // v-pre 指令
(!node.hasBindings && // 没有动态绑定
!node.if && !node.for && // 没有 v-if/v-for
!isBuiltInTag(node.tag) && // 不是内置标签
isPlatformReservedTag(node.tag) && // 是平台保留标签
!isDirectChildOfTemplateFor(node) && // 不是 template v-for 的直接子节点
Object.keys(node).every(isStaticKey)) // 所有属性都是静态的
)
}
3.静态根节点
markStaticRoots
函数用来标记静态根节点。
- 当节点是静态的且存在子节点时才会被标记为静态根;
- 但如果只有一个文本子节点,不会被标记为静态根(因为优化成本大于收益)。
js
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
4.静态节点规则
譬如如下的 html
结构:
html
<template>
<div> <!-- 非静态根节点 -->
<div>{{ msg }}</div> <!-- 非静态节点 -->
<div> <!-- 静态根节点 -->
<span>节点1</span> <!-- 非静态根节点 -->
</div>
<div> <!-- 静态根节点 -->
<span>节点2</span> <!-- 非静态根节点 -->
</div>
<div v-show="show"> <!-- 非静态根节点 -->
<span>节点3</span> <!-- 非静态根节点 -->
</div>
</div>
</template>
解析和优化代码如下:
js
import { optimize } from '../optimizer'
import { parse } from './parser'
import { generate } from '../codegen/index'
const template = `<div>
<div>{{ msg }}</div>
<div>
<span>节点1</span>
</div>
<div>
<span>节点2</span>
</div>
<div v-show="show">
<span>节点3</span>
</div>
</div>`
const ast = parse(template, {})
optimize(ast, {
isReservedTag: (tag: string) => true
})
const optimizeResult = generate(ast, {})
console.log('optimizeResult', optimizeResult, ast)
ast
打印结构如下,可以看出每个节点上的 static
和 staticRoot
属性:
optimizeResult
打印结果如下,静态节点渲染字符串会额外放置在 staticRenderFns
数组中:
js
{
"render": "with(this){return _c('div',[_c('div',[_v(_s(msg))]),_v(\" \"),_m(0),_v(\" \"),_m(1),_v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(show),expression:\"show\"}]},[_c('span',[_v(\"节点3\")])],1)],1)}",
"staticRenderFns": [
"with(this){return _c('div',[_c('span',[_v(\"节点1\")])],1)}",
"with(this){return _c('div',[_c('span',[_v(\"节点2\")])],1)}"
]
}