Skip to content

优化器的目标是遍历生成的模板 AST(抽象语法树),检测纯静态的子树,即那些永远不需要改变的 DOM 部分。

当被标记成静态子树后,可以:

  1. 将它们提升为常量,这样在每次重新渲染时就不需要重新创建节点;
  2. 在更新(patching)过程中完全跳过这些静态节点。

这种优化对于提升 Vue 应用的性能很重要,特别是在有大量静态内容的页面中。通过跳过静态内容的更新检查,可以显著提高渲染性能。

1.核心函数

optimize 函数会优先遍历一遍所有节点,然后标记上静态节点。然后再遍历一遍节点,如果某节点下全部是静态节点,则该节点被标记为静态根节点

核心函数逻辑如下:

js
export function optimize(root: ASTElement, options: CompilerOptions) {
  // 1. 标记静态节点
  markStatic(root)
  // 2. 标记静态根节点
  markStaticRoots(root, false)
}

2.静态节点

markStatic 函数用来标记静态节点。

  1. 判断节点是否是静态的;
  2. 递归处理子节点;
  3. 如果子节点不是静态的,父节点也标记为非静态
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 函数用来标记静态根节点。

  1. 当节点是静态的且存在子节点时才会被标记为静态根;
  2. 如果只有一个文本子节点,不会被标记为静态根(因为优化成本大于收益)
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 打印结构如下,可以看出每个节点上的 staticstaticRoot 属性:

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)}"
  ]
}