Vue 原始碼解讀(9)—— 編譯器 之 優化

李永寧發表於2022-03-04

前言

上一篇文章 Vue 原始碼解讀(8)—— 編譯器 之 解析 詳細詳解了編譯器的第一部分,如何將 html 模版字串編譯成 AST。今天帶來編譯器的第二部分,優化 AST,也是大家常說的靜態標記。

目標

深入理解編譯器的靜態標記過程

原始碼解讀

入口

/src/compiler/index.js

/**
 * 在這之前做的所有的事情,只有一個目的,就是為了構建平臺特有的編譯選項(options),比如 web 平臺
 * 
 * 1、將 html 模版解析成 ast
 * 2、對 ast 樹進行靜態標記
 * 3、將 ast 生成渲染函式
 *    靜態渲染函式放到  code.staticRenderFns 陣列中
 *    code.render 為動態渲染函式
 *    在將來渲染時執行渲染函式得到 vnode
 */
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 將模版解析為 AST,每個節點的 ast 物件上都設定了元素的所有資訊,比如,標籤資訊、屬性資訊、插槽資訊、父節點、子節點等。
  // 具體有那些屬性,檢視 start 和 end 這兩個處理開始和結束標籤的方法
  const ast = parse(template.trim(), options)
  // 優化,遍歷 AST,為每個節點做靜態標記
  // 標記每個節點是否為靜態節點,然後進一步標記出靜態根節點
  // 這樣在後續更新的過程中就可以跳過這些靜態節點了
  // 標記靜態根,用於生成渲染函式階段,生成靜態根節點的渲染函式
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 從 AST 生成渲染函式,生成像這樣的程式碼,比如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

optimize

/src/compiler/optimizer.js

/**
 * 優化:
 *   遍歷 AST,標記每個節點是靜態節點還是動態節點,然後標記靜態根節點
 *   這樣在後續更新的過程中就不需要再關注這些節點
 */
export function optimize(root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  /**
   * options.staticKeys = 'staticClass,staticStyle'
   * isStaticKey = function(val) { return map[val] }
   */
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  // 平臺保留標籤
  isPlatformReservedTag = options.isReservedTag || no
  // 遍歷所有節點,給每個節點設定 static 屬性,標識其是否為靜態節點
  markStatic(root)
  // 進一步標記靜態根,一個節點要成為靜態根節點,需要具體以下條件:
  // 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根
  // 靜態根節點不能只有靜態文字的子節點,因為這樣收益太低,這種情況下始終更新它就好了
  markStaticRoots(root, false)
}

markStatic

/src/compiler/optimizer.js

/**
 * 在所有節點上設定 static 屬性,用來標識是否為靜態節點
 * 注意:如果有子節點為動態節點,則父節點也被認為是動態節點
 * @param {*} node 
 * @returns 
 */
function markStatic(node: ASTNode) {
  // 通過 node.static 來標識節點是否為 靜態節點
  node.static = isStatic(node)
  if (node.type === 1) {
    /**
     * 不要將元件的插槽內容設定為靜態節點,這樣可以避免:
     *   1、元件不能改變插槽節點
     *   2、靜態插槽內容在熱過載時失敗
     */
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      // 遞迴終止條件,如果節點不是平臺保留標籤  && 也不是 slot 標籤 && 也不是內聯模版,則直接結束
      return
    }
    // 遍歷子節點,遞迴呼叫 markStatic 來標記這些子節點的 static 屬性
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      // 如果子節點是非靜態節點,則將父節點更新為非靜態節點
      if (!child.static) {
        node.static = false
      }
    }
    // 如果節點存在 v-if、v-else-if、v-else 這些指令,則依次標記 block 中節點的 static
    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
        }
      }
    }
  }
}

isStatic

/src/compiler/optimizer.js

/**
 * 判斷節點是否為靜態節點:
 *  通過自定義的 node.type 來判斷,2: 表示式 => 動態,3: 文字 => 靜態
 *  凡是有 v-bind、v-if、v-for 等指令的都屬於動態節點
 *  元件為動態節點
 *  父節點為含有 v-for 指令的 template 標籤,則為動態節點
 * @param {*} node 
 * @returns boolean
 */
function isStatic(node: ASTNode): boolean {
  if (node.type === 2) { // expression
    // 比如:{{ msg }}
    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)
  ))
}

markStaticRoots

/src/compiler/optimizer.js

/**
 * 進一步標記靜態根,一個節點要成為靜態根節點,需要具體以下條件:
 * 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根
 * 靜態根節點不能只有靜態文字的子節點,因為這樣收益太低,這種情況下始終更新它就好了
 * 
 * @param { ASTElement } node 當前節點
 * @param { boolean } isInFor 當前節點是否被包裹在 v-for 指令所在的節點內
 */
function markStaticRoots(node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      // 節點是靜態的 或者 節點上有 v-once 指令,標記 node.staticInFor = true or false
      node.staticInFor = isInFor
    }

    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      // 節點本身是靜態節點,而且有子節點,而且子節點不只是一個文字節點,則標記為靜態根 => node.staticRoot = true,否則為非靜態根
      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)
      }
    }
    // 如果節點存在 v-if、v-else-if、v-else 指令,則為 block 節點標記靜態根
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

總結

  • 面試官 問:簡單說一下 Vue 的編譯器都做了什麼?

    Vue 的編譯器做了三件事情:

    • 將元件的 html 模版解析成 AST 物件

    • 優化,遍歷 AST,為每個節點做靜態標記,標記其是否為靜態節點,然後進一步標記出靜態根節點,這樣在後續更新的過程中就可以跳過這些靜態節點了;標記靜態根用於生成渲染函式階段,生成靜態根節點的渲染函式

    • 從 AST 生成執行渲染函式,即大家說的 render,其實還有一個,就是 staticRenderFns 陣列,裡面存放了所有的靜態節點的渲染函式


  • 面試官:詳細說一下靜態標記的過程

    • 標記靜態節點

      • 通過遞迴的方式標記所有的元素節點

      • 如果節點本身是靜態節點,但是存在非靜態的子節點,則將節點修改為非靜態節點

    • 標記靜態根節點,基於靜態節點,進一步標記靜態根節點

      • 如果節點本身是靜態節點 && 而且有子節點 && 子節點不全是文字節點,則標記為靜態根節點

      • 如果節點本身不是靜態根節點,則遞迴的遍歷所有子節點,在子節點中標記靜態根


  • 面試官:什麼樣的節點才可以被標記為靜態節點?

    • 文字節點

    • 節點上沒有 v-bind、v-for、v-if 等指令

    • 非元件

連結

感謝各位的:關注點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注點贊收藏評論

新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

相關文章