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

什么时候使用编译?

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

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

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

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

parse 过程简述

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

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

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

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

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

optimize 过程

parse 过程后,会输出生成 AST 树,那么接下来咱们须要对这颗树作优化。为何须要作优化呢?缓存

由于 Vue 是数据驱动,是响应式的。可是咱们的模板中,并非全部的数据都是响应式的,也有不少的数据在首次渲染以后就永远不会变化了。既然如此,在咱们执行 patch 的时候就能够跳过这些非响应式的比对。babel

简单来讲:整个 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的手写实现

相关文章
相关标签/搜索