原作者:Soon Wang
什么是 Optimizer
上一章我们探索了 Parser 解析器部分,其解析得到的 AST 会作为输入传递给 Optimizer 继续处理,Optimizer 是优化器的意思,所以肯定是会对 AST 进行优化,那具体要优化什么呢? 这里需要先认识 Vue 模板中的一个重要概念:“静态节点”
静态节点
在 Vue 中,静态节点是指在模板中不会随数据变化而改变的内容。例如,纯文本节点或未绑定任何动态属性的元素都属 静态节点。相反,包含数据绑定或动态属性的节点则被称为动态节点
我们知道 Vue 的核心理念是数据驱动的视图更新,即视图层会根据数据层的变化自动进行更新,这得益于 Vue 响应式系统,它能追踪数据变化并更新对应 DOM。然而,对于静态节点来说,其渲染内容并不依赖于数据变化,频繁地检查和更新这些节点实际上是不必要且浪费性能的
优化目的
Optimizer 优化器的目的就是识别并标记出 AST 中的静态节点。这样一来,在运行时 patch 阶段,可以跳过对静态节点的虚拟 DOM Diff 对比,避免不必要的计算和 DOM 操作,提升运行时渲染性能
Optimizer 的工作流程
Optimizer 的核心流程并不复杂,可以简单概括为以下两步:
- 标记静态节点:第一次遍历
AST,标记当前节点是否为静态节点 - 标记静态根节点:第二次遍历
AST,标记当前节点是否为静态根节点
Optimizer 源码架构流程如下图所示:

源码解析
optimize 入口方法
export function optimize(
root: ASTElement | null | undefined,
options: CompilerOptions,
) {
if (!root) return
// 生成静态 key 的映射函数,用于判断静态节点
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 生成平台保留标签的映射函数,用于判断静态节点
isPlatformReservedTag = options.isReservedTag || no
// 1. 第一次遍历:标记所有非静态节点
markStatic(root)
// 2. 第二次遍历:标记静态根节点
markStaticRoots(root, false)
}入口方法很简洁,正如前面整体流程中的介绍,其主要包含两个方法 markStatic 和 markStaticRoots
markStatic 标记静态节点
function markStatic(node: ASTNode) {
// 首先执行 isStatic 判断一次是否静态节点
node.static = isStatic(node)
if (node.type === 1) {
// 不要将组件插槽内容标记为静态,从而避免:
// 1. 组件无法更改插槽节点
// 2. 静态插槽内容在热重载时失败
if (
!isPlatformReservedTag(node.tag)
&& node.tag !== 'slot'
&& node.attrsMap['inline-template'] == null
) {
return
}
// 递归标记子节点
// 如果任一子节点不是静态的,则父节点(当前节点)也标记为非静态
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
// 递归标记条件块中的所有子节点
// 如果任一子节点不是静态的,则父节点(当前节点)也标记为非静态
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}markStatic 标记方法主要分为三步骤:
- 通过
isStatic方法判断是否是静态节点(具体判断见下文) - 递归标记子节点:如果任一子节点不是静态的,则回溯时候父节点(当前节点)也标记为非静态
- 递归标记条件块中的所有子节点:如果任一子节点不是静态的,则父节点(当前节点)也标记为非静态
isStatic 静态节点判断标准
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// expression
return false
}
if (node.type === 3) {
// text
return true
}
return !!(
node.pre
|| (!node.hasBindings // no dynamic bindings
&& !node.if
&& !node.for // not v-if or v-for or v-else
&& !isBuiltInTag(node.tag) // not a built-in
&& isPlatformReservedTag(node.tag) // not a component
&& !isDirectChildOfTemplateFor(node)
&& Object.keys(node).every(isStaticKey))
)
}
/**
* 判断节点是否是模板 for 的直接子节点
* 如果一个节点是 v-for 指令的直接子节点,
* 它的内容可能是动态的,不能被错误地标记为静态节点
*/
function isDirectChildOfTemplateFor(node: ASTElement): boolean {
while (node.parent) {
node = node.parent
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}可见,判断静态节点主要有以下几个标准:
- 表达式节点(
type === 2)不是静态节点 - 纯文本节点(
type === 3)是静态节点 - 对于元素节点(
type === 1),使用v-pre指令的节点被视为静态节点 - 对于元素节点(
type === 1),且满足以下条件才是静态节点:- 没有动态绑定(
hasBindings为false) - 没有
v-if、v-for、v-else等指令 - 不是内置标签(如
slot、component) - 是平台保留标签(如
div、span等HTML标签) - 不是带有
v-for指令的template标签的直接子节点 - 节点的所有属性都属于静态键(
isStaticKey)
- 没有动态绑定(
markStaticRoot 标记静态根节点
function markStaticRoots(node: ASTNode, isInFor: boolean) {
// 剪枝:只处理 type === 1 元素类型节点,因为只有元素节点可以有子节点
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// 如果节点没有子节点,或者仅有一个静态文本类型的子节点,
// 那么不应当标记为静态根节点,因为提取出来的成本将超过收益,反而不如直接渲染
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)
}
}
}
}markStaticRoot 标记方法同样可以分为三步:
- 判断当前节点是否是静态根节点
- 递归标记子节点,但子节点不会回溯影响父节点
- 递归标记条件块中的所有子节点,但子节点不会回溯影响父节点
其中,对于静态根节点的判断标准是同时满足以下三个条件:
- 当前节点是静态节点
- 有至少一个子节点
- 子节点不能只是一个纯文本节点
举个例子:
- 非静态根节点:
<div></div>、<div>world</div>,因为前者没有子节点,而后者只有一个纯文本子节点,均不满足上述条件 - 属于静态根节点:
<div><span>world</span></div>,满足上述条件
另外,还有两点值得注意的是:
markStaticRoot方法开头的if (node.type === 1)条件判断其实是剪枝优化:只需要处理node.type === 1元素类型节点,因为只有元素节点可以有子节点,否则不需要标记可以剪枝减少计算markStaticRoot方法除了标记静态根节点,还会标记staticInFor,判断依据是:- 对于已经标记为静态或者包含
v-once指令的节点,node.staticInFor = isInFor,其中isInFor是方法第二个入参
- 对于已经标记为静态或者包含
实践
现有模板如下,预期只有 <span> 和 word 两个节点是静态节点,不存在静态根节点
<div>
<span>world</span><h1>{{ msg }}</h1>
</div>经过 Optimizer 优化前的 AST (Parser 解析输出)如下:
{
"type": 1,
"tag": "div",
"attrsList": [],
"attrsMap": {},
"rawAttrsMap": {},
"parent": undefined,
"children": [
{
"type": 1,
"tag": "span",
"attrsList": [],
"attrsMap": {},
"rawAttrsMap": {},
"parent": [Circular *1],
"children": [
{
"type": 3,
"text": "world",
"static": true
}
],
"plain": true,
}
{
"type": 1,
"tag": "h1",
"attrsList": [],
"attrsMap": {},
"rawAttrsMap": {},
"parent": [Circular *1],
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [],
"text": "{{ msg }}",
"static": false
}
],
"plain": true,
}
],
"plain": true,
}经过 Optimizer 优化后的 AST 如下,注意其中添加了 static、staticRoot、staticInFor 属性
{
"type": 1,
"tag": "div",
// 省略属性 ..
"children": [
{
"type": 1,
"tag": "span",
// 省略属性 ..
"children": [
{
"type": 3,
"text": "world",
"static": true
}
],
"static": true,
"staticInFor": false,
"staticRoot": false
}
{
"type": 1,
"tag": "h1",
// 省略属性 ..
"children": [
{
"type": 2,
"expression": "_s(msg)",
"tokens": [],
"text": "{{ msg }}",
"static": false
}
],
"static": false,
"staticRoot": false
}
],
"static": false,
"staticRoot": false
}总结
通过对优化器(Optimizer)的源码解析,我们了解到 Vue2 是如何在编译阶段识别和标记静态节点的,优化器的工作为后续的代码生成阶段奠定了基础,在下一章的生成器(Generator)部分,我们将进一步探讨 Vue2 是如何具体优化这些静态节点的,实现更高效的渲染性能