【Vue2深度学习】模板编译篇-优化器

280 阅读5分钟

通过解析器篇章的学习,我们知道,解析器的作用是将HTML模板解析成AST,而本章要介绍的优化器,则主要用来在AST中找出静态节点并打上标记。下面,我们通过提问回答的方式,详细了解一下优化器相关概念及工作原理。

一、模板编译中为什么需要优化器

理论上讲,有了AST就可直接生成render函数了。为什么还需要优化器呢?这当然是为了性能考虑。

我们通过优化器,找出静态节点,可以实现以下两点好处:

  • 每次重新渲染时,不需要为静态节点创建新节点。

    在前面虚拟DOM的学习中,我们知道,每次重新渲染,都会使用最新的状态生成一份全新的VNode与旧的VNode进行对比。而在生成vode的过程中,如果发现一个节点被标记为静态节点,那么除了首次渲染会生成节点外,在重新渲染时并不会生成新的子节点,而是克隆已经存在的静态节点。

  • 在虚拟DOM的patch过程中可以跳过。

    patch过程中,如果两个节点都是静态节点,就不需要进行对比与更新DOM的操作,而是可以直接跳过,这样既节省了JS运算成本,又减少了DOM的操作。

二、优化器所寻找的静态节点到底是什么

所谓静态节点,是指那些在AST中永远都不会发生变化的节点,也就是说,它一旦首次渲染成了,之后不管状态如何改变,它都不会变化了。例如:

<ul>
  <li>我是静态节点,我永远不会发生变化</li>
  <li>我是静态节点,我永远不会发生变化</li>
</ul>

在上面代码中,每个li标签里的内容都是不含任何变量的文本,它不随着状态改变而改变,所以该li标签就是一个静态节点。

而li标签的父节点ul标签,因其所有子节点都是静态节点,所以它也是静态节点,我们称之为静态根节点。

三、优化器内部是如何运作的

优化器的内部实现主要分为两个步骤:

1.在AST中找出所有静态节点并打上标记 2.在AST中找出所有静态根节点并打上标记;

落实到源码中,代码是这样实现的。

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

四、优化器如何标记静态节点

要想从AST中找出所有静态节点,我们只需从根节点开始,先标记根节点是否为静态节点,然后看根节点如果是元素节点,那么就去向下递归它的子节点,子节点如果还有子节点那就继续向下递归,直到标记完所有节点。

所谓标记静态节点,就是为该节点加上static属性,如果该节点是静态节点,则属性值为true,如果不是静态节点,则属性值为fasle。源码如下:

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) { // node.type为1时,代表该节点时元素节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child) 
    } 
  }
}

那么,如何找出一个静态节点呢?isStatic函数是关键,具体如下:

function isStatic (node: ASTNode): boolean {
  // 当前节点为包含变量的动态文本节点
  if (node.type === 2) { 
    return false
  }
  // 当前节点为不包含变量的纯文本节点
  if (node.type === 3) { 
    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)
  ))
}

HTML解析器在调用钩子函数创建AST节点时会根据节点类型的不同为节点加上不同的type属性,来标记AST节点的节点类型。所以在判断一个节点是否为静态节点时首先会根据type值判断节点类型,如果type值为2,那么该节点是包含变量的动态文本节点,它就肯定不是静态节点,返回false

如果type值为2,那么该节点是不包含变量的纯文本节点,它就肯定是静态节点,返回true

如果type值为1,说明该节点是元素节点,那就需要进一步判断。

  • 如果节点使用了v-pre指令,那就断定它是静态节点;

  • 如果节点没有使用v-pre指令,那它要成为静态节点必须满足:

    • 不能使用动态绑定语法,即标签上不能有v-@:开头的属性;
    • 不能使用v-ifv-elsev-for指令;
    • 不能是内置组件,即标签名不能是slotcomponent
    • 标签名必须是平台保留标签,即不能是组件;
    • 当前节点的父节点不能是带有 v-for 的 template 标签;
    • 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一。

五、优化器如何标记静态根节点

所谓标记静态根节点,就是为该节点加上staticRoot属性,如果该节点是静态根节点,则属性值为true,如果不是静态根节点,则属性值为fasle。

寻找静态根节点与寻找静态节点的逻辑类似,都是从AST根节点递归向下遍历寻找。其代码如下:

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    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)
      }
    }
  }
}

从代码我们可以看到,一个节点要想成为静态根节点,它必须满足以下要求:

  • 节点本身必须是静态节点;
  • 必须拥有子节点 children
  • 子节点不能只是只有一个文本节点;

如果当前节点不是静态根节点,那就继续递归遍历它的子节点。