编译三:optimize

133 阅读3分钟

在模板编译整个过程中,涉及到三个阶段:parseoptimizegenerate 。在《编译二: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)
}

函数接收两个参数:

  • rootASTElement 类型,即 AST 树
  • optionsCompilerOptions 类型,可选项

从代码可看出,核心逻辑有两点:

  • 标记静态节点
  • 标记静态根节点

那么,接下来一步一步地来分析它们是如何标记节点的。

标记静态节点

// 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-ifv-forv-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.statictrue 或者 node.oncetrue ,则设置 node.staticInFortrue

接着,如果当前节点满足三个条件,则该节点为 静态根节点,即 node.staticRoottrue;否则为 false

然后,如果当前节点存在子节点,则递归调用 markStaticRoots 处理子节点,判断它们是否为静态根节点。

最后,如果节点存在 v-if 指令,也递归调用对它的子节点进行处理。

可以看出,其逻辑实现与 markStatic 类似。

参考

optimize