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的手寫實現

相關文章
相關標籤/搜索