在模板编译整个过程中,涉及到三个阶段:parse、optimize、generate 。在《编译二:parse》一文中已经分析过模板解析为 AST 树过程;那么,本文将分析编译过程第二个阶段:optimize,即对 AST 树进行优化。
那为什么需要对 AST 树进行优化呢?当数据发生变化,触发重新渲染时,patch 会对比 DOM 是否发生变化,进而变更视图;然而,并非所有的数据都是响应式的,比如常量,这类数据在首次渲染后则不会发生改变,其 DOM 也不会发生变化,那么,patch 完全可以跳过这类数据 DOM 的对比,以此来提升性能。
因此,optimize 的作用是将不会发生变化的数据标记为静态。
optimize
// src/compiler/optimizer.js
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
函数接收两个参数:
root:ASTElement类型,即 AST 树options:CompilerOptions类型,可选项
从代码可看出,核心逻辑有两点:
- 标记静态节点
- 标记静态根节点
那么,接下来一步一步地来分析它们是如何标记节点的。
标记静态节点
// src/compiler/optimizer.js
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
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
}
}
}
}
}
isStatic 的作用是判断节点是否为静态节点,具体实现如下:
// src/compiler/optimizer.js
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)
))
}
从代码实现可看出,如果节点类型为表达式,则为非静态节点;如果节点类型为纯文本,则为静态节点;
如果节点有属性 pre ,即具有指令 v-pre 时,则为静态节点;
如果同时满足以下条件:
- 没有动态绑定属性,比如
:class - 没有指令
v-if、v-for、v-else - 非内置标签
- 非内置组件
- 非带有
v-for指令的template的直接子节点 - 节点所有属性的
key都满足静态key
则为静态节点;否则为非静态节点。
回到 markStatic,如果 node.type === 1 时,则进入 if 逻辑代码块。核心的逻辑有两点:
-
如果节点存在子节点,即
node.children.length > 0,则遍历children,同时调用markStatic对子节点进行标记;可见,在标记静态节点的过程中,采用深度遍历每个节点,判断其是否为静态节点,对其进行标记,即设置属性static值为true或者false。如果存在子节点为非静态节点,则需要重新将父节点
static设置为false,即非静态节点;也就是说,父节点是否为静态节点,是由其所有子节点决定的。 -
如果节点
ifConditions不为空,对其进行遍历,获取到v-if指令对应的所有条件block,递归执行markStatic;在递归执行过程中,如果存在block为非静态节点;那么,其父节点则为静态节点。
回到 optimize,分析完标记静态节点逻辑,接着来分析标记静态根节点。
标记静态根节点
// src/compiler/optimizer.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)
}
}
}
}
从代码实现可看出,如果 node.type === 1 ,则执行 if 逻辑;如果 node.static 为 true 或者 node.once 为 true ,则设置 node.staticInFor 为 true。
接着,如果当前节点满足三个条件,则该节点为 静态根节点,即 node.staticRoot 为 true;否则为 false。
然后,如果当前节点存在子节点,则递归调用 markStaticRoots 处理子节点,判断它们是否为静态根节点。
最后,如果节点存在 v-if 指令,也递归调用对它的子节点进行处理。
可以看出,其逻辑实现与 markStatic 类似。