源码分析:Vue 编译(compile)核心流程之optimize

667 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue编译的核心主要包括三步:生成AST树,优化AST树,生成最终代码。之前我们分析了Vue编译核心流程的parse过程(点击这里跳转),本节我们来分析编译核心流程的第二步:optimize(优化AST树)。

optimize(AST树的优化)

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 根据template生成AST树
  const ast = parse(template.trim(), options)
  // 优化AST树
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 生成代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

optimize的目的是给生成的AST树添加静态根节点标记,因为我们定义的模板中并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化,所以这部分数据生成的DOM也不会变化,有了静态节点标记之后,我们在重新渲染diff的过程中就可以跳过对他们的比对。

optimize函数定义在src/compiler/optimizer.js中:

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  // 如果ast对象不存在,直接返回
  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)
}

可以看到,optimize的逻辑最主要就是执行了两个函数,目的就是标记静态节点与标记静态根。

markStatic

function markStatic (node: ASTNode) {
  // 判断当前节点是否是static节点
  node.static = isStatic(node)
  // 如果是普通元素AST
  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
    // 是一个组件节点且不是slot且inline-template属性为null,则直接返回,不执行下面的逻辑
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    // 遍历children,递归执行markStatic
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      // 如果child中有非静态节点,当前节点也标记为非静态节点
      if (!child.static) {
        node.static = false
      }
    }
    if (node.ifConditions) {
      // 如果node节点中有ifConditions属性,遍历ifConditions数组,执行markStatic
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        // 如果block中有非静态节点,当前节点也标记为非静态节点
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}
function isStatic(node: ASTNode): boolean {
  // 表达式节点,返回false
  if (node.type === 2) { // expression
    return false
  }
  // 纯文本节点,返回true
  if (node.type === 3) { // text
    return true
  }
  // 或者满足下面的所有条件,比如含有pre指令或者(没有hasBindings、if、for等属性)条件,才是静态节点,其他情况返回false
  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)
  ))
}

标记静态节点的流程还是较为简单的,首先会判断当前节点是否是静态节点,再继续判断children,递归执行markStatic函数,标记children是否是静态节点,如果children中有非静态节点,则所有的祖先节点标记为非静态节点;再继续遍历ifConditions数组,同样递归执行markStatic函数,标记其中节点,如果数组中的有非静态节点,则所有祖先节点标记为非静态节点。

完成所有节点的静态标记之后,去标记静态根节点,执行markStaticRoots函数:

markStaticRoots

// second pass: mark static roots.
// 标记静态根
markStaticRoots(root, false)

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.
    // 如果节点只有一个children且这个children为文本节点,不要给他标记staticRoot为true,如果这样做的话,成本会大于收益
    // 如果static为true且含有children,且不是上述这种情况,则标记staticRoot为true
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      // 满足条件直接return,否则继续检验children
      return
    } else {
      node.staticRoot = false
    }
    // 遍历children,递归执行markStaticRoots,标记静态根
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    // 遍历ifConditions,执行markStaticRoots,标记静态根
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

这儿的逻辑也比较简单,从根节点开始,递归遍历整个树,找到所有满足条件的静态根节点。

总结

到此为止,我们了解了optimize的整个过程,主要就是递归遍历整个ast树,首先对满足条件的节点,标记为静态节点,再根据标记静态节点的树,再做一次遍历,标记所有的静态根节点。