Vue 源码阅读(九):编译过程的optimize 阶段

2,834 阅读1分钟

何时使用编译?

$mount的时候,当遇到 Vue 实例传入的参数不包含 render,而是 template 或者 el 的时候,就会执行编译的过程,将另外两个转变为 render 函数。

在编译的过程中,有三个阶段:

  • parse : 解析模板字符串生成 AST (抽象语法树)
  • optimize:优化语法树
  • generate:生成 render 函数代码

本文只针对其中的 optimize 阶段进行重点阐述。

parse 过程简述

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。

parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型:

  • type 为 1 表示是普通标签元素
  • type 为 2 表示是表达式
  • type 为 3 表示是纯文本

optimize 过程

parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化。为什么需要做优化呢?

因为 Vue 是数据驱动,是响应式的。但是我们的模板中,并不是所有的数据都是响应式的,也有很多的数据在首次渲染之后就永远不会变化了。既然如此,在我们执行 patch 的时候就可以跳过这些非响应式的比对。

简单来说:整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, false) 标记静态根节点。

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

标记静态节点

通过代码来看,可以更好解析标记静态节点的逻辑:

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

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()中我们看到,isBuiltInTag(即tagcomponentslot)的节点不会被标注为静态节点,isPlatformReservedTag(即平台原生标签,web 端如 h1 、div标签等)也不会被标注为静态节点。
  • 如果一个节点是普通标签元素,则遍历它的所有children,执行递归的markStatic
  • 代码node.ifConditions表示的其实是包含有elseifelse 子节点,它们都不在children中,因此对这些子节点也执行递归的markStatic
  • 在这些递归过程中,只要有子节点不为static的情况,那么父节点的static属性就会变为 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.
    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()方法针对的都是普通标签节点。表达式节点与纯文本节点都不在考虑范围内。
  • 上一步方法markStatic()得出的static属性,在该方法中用上了。将每个节点都判断了一遍static属性之后,就可以更快地确定静态根节点:通过判断对应节点是否是静态节点 内部有子元素 单一子节点的元素类型不是文本类型。

注意:只有纯文本子节点时,他是静态节点,但不是静态根节点。静态根节点是 optimize 优化的条件,没有静态根节点,说明这部分不会被优化。

而 Vue 官方说明是,如果子节点只有一个纯文本节点,若进行优化,带来的成本就比好处多了。因此这种情况下,就不进行优化。

问题:为什么子节点的元素类型是静态文本类型,就会给 optimize 过程加大成本呢?

首先来分析一下,之所以在 optimize 过程中做这个静态根节点的优化,目的是什么,成本是什么?

目的:在 patch 过程中,减少不必要的比对过程,加速更新。

目的很好理解。那么成本呢?

成本:a. 需要维护静态模板的存储对象。b. 多层render函数调用.

详细解释这两个成本背后的细节:

维护静态模板的存储对象

一开始的时候,所有的静态根节点 都会被解析生成 VNode,并且被存在一个缓存对象中,就在 Vue.proto._staticTree 中。

随着静态根节点的增加,这个存储对象也会越来越大,那么占用的内存就会越来越多

势必要减少一些不必要的存储,所有只有纯文本的静态根节点就被排除了

多层render函数调用

这个过程涉及到实际操作更新的过程。在实际render 的过程中,针对静态节点的操作也需要调用对应的静态节点渲染函数,做一定的判断逻辑。这里需要一定的消耗。

综合所述

如果纯文本节点不做优化,那么就是需要在更新的时候比对这部分纯文本节点咯?这么做的代价是什么呢?只是需要比对字符串是否相等而已。简直不要太简单,消耗简直不要太小。

既然如此,那么还需要维护多一个静态模板缓存么?在 render 操作过程中也不需要额外对该类型的静态节点进行处理。

staticRoot 的具体使用场景

staticRoot 属性会在我们编译过程的第三个阶段generate阶段--生成 render 函数代码阶段--起到作用。generate函数定义在src/compiler/codegen/index.js中,我们详细来看:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

generate 函数首先通过 genElement(ast, state) 生成 code,再把 codewith(this){return ${code}}} 包裹起来。这里的genElement代码如下:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      const data = el.plain ? undefined : genData(el, state)

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

其中,首个判断条件就用到了节点的staticRoot属性:

if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  }

进入genStatic:

// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

可以看到,genStatic函数最终将对应的代码逻辑塞入到了state.staticRenderFns中,并且返回了一个带有_m函数的字符串,这个_m是处理静态节点函数的缩写,为了方便生成的 render 函数字符串不要过于冗长。其具体的含义在src/core/instance/render-helpers/index.js中:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

/**
 * Runtime helper for rendering static trees.
 */
export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

在具体执行 render 函数的过程中,会执行_m函数,其实执行的就是上面代码中的renderStatic函数。静态节点的渲染逻辑是这样的:

  • 首先,查找缓存:查看当前 Vue 实例中的_staticTrees属性是否有对应的index缓存值,若有,则直接使用。
  • 否则,则会调用 Vue 实例中的$options.staticRenderFns对应的函数,结合genStatic的代码,可知其对应执行的函数详细。

总结

在本文中,我们详细分析了 Vue 编译过程中的 optimize 过程。这个过程主要做了两个事情:标记静态节点markStatic与标记静态根节点markStaticRoot。同时,我们也分析了标记静态根节点markStaticRoot在接下来的 generate 阶段的作用。

希望对读者有一定的帮助!若有理解不足之处,望指出!


vue源码解读文章目录:

(一):Vue构造函数与初始化过程

(二):数据响应式与实现

(三):数组的响应式处理

(四):Vue的异步更新队列

(五):虚拟DOM的引入

(六):数据更新算法--patch算法

(七):组件化机制的实现

(八):计算属性与侦听属性

(九):编译过程的 optimize 阶段

Vue 更多系列:

Vue的错误处理机制

以手写代码的方式解析 Vue 的工作过程

Vue Router的手写实现