optimize优化

在经过parse模板编译完成后,我们可以得到一个ast树形结构,接下来进行optimize优化第二大步骤。这个过程相比较其它两个步骤,是最简单的。

import { parse } from './parser/index'
import { optimize } from './optimizer'

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // ...
}

优化的目的在于:在编译的时候有些节点自首次渲染完毕后,在后续的派发更新过程中不会随着数据的变动而变动,因此我们在进行节点对比的时候,可以直接跳过这些节点,进一步加快组件渲染的速度。

optimize优化的过程中,它的处理方式是深度遍历ast树形结构,遇到静态节点的时候把它的ast.static属性设置为true。同时对于一个父ast节点来说,当其children子节点全部为静态节点的时候,那么其本身也是一个静态节点,我们把它的ast.staticRoot设置为true

在介绍optimize优化这个章节的时候,我们以下面这个例子为例:

let html = `
  <div>
    <p>{{msg}}</p>
    <span>静态节点</span>
  </div>
`

在以上例子中,span节点因为其内容是纯文本,因此它的ast.static一定为true

我们回过头来看一下optimize方法的定义,其代码如下:

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

在这个方法中,它首先调用markStatic标记静态节点,然后调用markStaticRoots来标记静态根节点。

静态节点类型

markStatic方法中,它首先调用isStatic方法来判断当前ast是否为一个静态节点,其代码如下:

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  // ...
}
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)
  ))
}

在阅读完isStatic方法后,我们发现静态节点必须满足以下几种情况:

  1. 带插值(表达式)的文本节点,不是静态节点,例如:
// 不是静态节点
let html = `<div>{{msg}}</div>`
  1. 纯文本节点,是静态节点,例如:
// 是静态节点
let html = `<div>Hello, Vue.js</div>`
  1. 如果是普通元素节点,并且使用了v-pre指令,则是静态节点,例如:
// 是静态节点
let html = `<div v-pre>{{msg}}</div>`
  1. 如果是普通元素,在没有使用v-pre指令的情况下,还必须同时满足:没有动态绑定属性、没有使用v-if、没有使用v-for、不是内置组件slot/component、是平台保留标签、不是带有v-fortemplate标签的直接子节点、节点的所有属性的key都是静态key,例如:
// 是静态节点
let html = '<div class="box"></div>'

标记静态节点

在分析完isStatic方法后,我们来分析一下markStatic标记静态节点方法的实现原理,其代码如下:

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
        }
      }
    }
  }
}

代码分析:

  • type=2type=3的时候,代表它们分别是带表达式的插值文本和纯文本,这个时候使用isStatic方法的返回结果,直接标记static属性为true即可。
  • 对于type=1,它是普通元素节点的时候,判断逻辑稍微复杂一点。第一步首先需要使用for循环去遍历children子节点,然后在for循环中递归调用markStatic方法,以达到深度遍历并标记子节点的目的。在这个过程中,唯一一个值得注意的地方就是,在对children子节点标记完毕后,会根据子节点的static属性来设置父节点的static属性。只要有一个子节点的static属性不为true,那么父节点也一定不为true。第二步,如果当前节点有v-if/v-else-if/v-else等指令,由于这些节点并不会保存在children数组中,而是在node.ifConditions属性下面,因此我们需要遍历ifConditions数组,来递归标记子节点。同样的,在标记完子节点后,我们需要根据子节点的static来同步更新父节点的static
const showMsg = false
let html = `
  <div>
    <div v-if="showMsg">show</div>
    <div v-else="showMsg">not show</div>
  </div>
`

const node = {
  ifConditions: [
    { exp: 'showMsg', block: 'div的ast节点' }{ exp: undefined, block: 'div的ast节点' }
  ]
}

标记静态根节点

在介绍完标记静态节点后,我们接着要介绍标记静态根节点markStaticRoots方法,其代码如下:

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

我们可以看到,在markStaticRoots方法中它对于children子节点和带v-if/v-else-if/v-else等指令的处理过程是类似的,我们省略这部分内容重复的介绍。

我们来看下面这段有意思的代码:

// 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
}

我们从注释中也可以看出来,如果当前节点static属性为true了,要标记它为静态根节点的话,还必须满足它的子节点不能只有一个纯文本节点,因为这样做其优化成本要大于其收益。

最后更新时间:
贡献者: wangtunan