首頁 > 軟體

Vue3 原始碼解讀靜態提升詳解

2022-08-19 18:00:46

什麼是靜態提升

靜態提升是Vue3編譯優化中的其中一個優化點。所謂的靜態提升,就是指在編譯器編譯的過程中,將一些靜態的節點或屬性提升到渲染函數之外。下面,我們通過一個例子來深入理解什麼是靜態提升。

假設我們有如下模板:

<div>
  <p>static text</p>
  <p>{{ title }}</p>
</div>

在沒有靜態提升的情況下,它對應的渲染函數是:

function render() {
  return (openClock(), createClock('div', null,  [
    createVNode('p', null, 'static text'),
    createVNode('p', null, ctx.title, 1 /* TEXT */)
  ]))
}

從上面的程式碼中可以看到,在這段虛擬DOM的描述中存在兩個 p 標籤,一個是純靜態的,而另一個擁有動態文字。當響應式資料 title 的值發生變化時,整個渲染函數會重新執行,併產生新的虛擬DOM樹。在這個過程中存在效能開銷的問題。原因是純靜態的虛擬節點在更新時也會被重新建立一次,其實這是沒有必要的。因此我們需要使用靜態提升來解決這個問題。

所謂的 “靜態提升”,就是將一些靜態的節點或屬性提升到渲染函數之外。如下面的程式碼所示:

// 把靜態節點提升到渲染函數之外
const hoist1 = createVNode('p', null, 'text')
function render() {
  return (openBlock(), createBlock('div', null, [
    hoist1, // 靜態節點參照
    createVNode('p', null, ctx.title, 1 /* TEXT */)
  ]))
}

可以看到,經過靜態提升後,在渲染函數內只會持有對靜態節點的參照。當響應式資料發生變化,並使得渲染函數重新執行時,並不會重新建立靜態的虛擬節點,從而避免了額外的效能開銷。

transform 轉換器

在模板編譯器將模板編譯為渲染函數的過程中,transform 函數扮演著十分重要的角色。它用來將模板AST轉換為 JavaScript AST。下面,我們來看看 transform 函數做了什麼。

// packages/compiler-core/src/transform.ts
// 將 模板AST 轉換為 JavaScript AST
export function transform(root: RootNode, options: TransformOptions) {
  // 建立轉換上下文
  const context = createTransformContext(root, options)
  // 遍歷所有節點,執行轉換
  traverseNode(root, context)
  // 如果編譯選項中開啟了 hoistStatic 選項,則進行靜態提升
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  // 建立 Block,收集所有動態子節點
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  // 確定最終的元資訊
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
  if (__COMPAT__) {
    root.filters = [...context.filters!]
  }
}

從上面的原始碼中可以看到,transform 函數的實現並不複雜。

  • 首先,呼叫 createTransformContext 函數建立了一個轉換上下文物件,該物件儲存著轉換過程中的一些上下文資料。例如當前正在轉換的節點 currentNode、當前轉換節點的父節點parent、用於替換當前正在轉換的節點的 replaceNode 函數、用於移除當前存取的節點的 removeNode 函數等。
  • 接著呼叫 traverseNode 函數,遞迴遍歷模板AST,將模板AST 轉換為 JavaScript AST。
  • 然後判斷編譯選項options中是否開啟了 hoistStatic,如果是,則進行靜態提升。
  • 接下來則建立 Block,收集模板中的動態子節點。
  • 最後做的事情則是確定最終的元資訊。

由於本文主要是介紹靜態提升,因此我們圍繞靜態提升的程式碼繼續往下探索,其餘部分程式碼將在其它文章中介紹。

hoistStatic 靜態提升

hoistStatic 函數的原始碼如下所示:

// packages/compiler-core/src/transforms/hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
  walk(
    root,
    context,
    // Root node is unfortunately non-hoistable due to potential parent
    // fallthrough attributes.
    // 根節點作為 Block 角色是不能被提升的
    isSingleElementRoot(root, root.children[0])
  )
}

可以看到,hoistStatic 函數接收兩個引數,第一個引數是根節點,第二個引數是轉換上下文。在該函數中,僅僅只是呼叫了 walk 函數來實現靜態提升。

並且在 walk 函數中呼叫 isSingleElementRoot 函數來告知 walk 函數根節點是不能提升的,原因是根節點作為 Block 角色是不可被提升的。

我們接下來繼續探究 walk 函數的原始碼。

walk 函數

靜態提升的真正實現邏輯在 walk 函數內,其原始碼如下所示:

function walk(
  node: ParentNode,
  context: TransformContext, // 轉換上下文物件
  doNotHoistNode: boolean = false  // 節點是否可以被提升
) {
  const { children } = node
  // 子節點的數量
  const originalCount = children.length
  // 可提升節點的數量
  let hoistedCount = 0
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    // only plain elements & text calls are eligible for hoisting.
    // 只有普通元素和文字才能被提升
    if (
      child.type === NodeTypes.ELEMENT &&
      child.tagType === ElementTypes.ELEMENT
    ) {
      // 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記
      // 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別
      const constantType = doNotHoistNode
        ? ConstantTypes.NOT_CONSTANT
        : getConstantType(child, context)
      // 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT
      if (constantType > ConstantTypes.NOT_CONSTANT) {
        // 如果節點可以被提升
        if (constantType >= ConstantTypes.CAN_HOIST) {
          // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升
          ;(child.codegenNode as VNodeCall).patchFlag =
            PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
            // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中
          child.codegenNode = context.hoist(child.codegenNode!)
          // 提升節點數量自增1
          hoistedCount++
          continue
        }
      } else {
        // node may contain dynamic children, but its props may be eligible for
        // hoisting.
        // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升
        const codegenNode = child.codegenNode!
        if (codegenNode.type === NodeTypes.VNODE_CALL) {
          // 獲取 patchFlag 修補程式標誌
          const flag = getPatchFlag(codegenNode)
          // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別
          // 並且該節點的 props 是可以被提升的
          if (
            (!flag ||
              flag === PatchFlags.NEED_PATCH ||
              flag === PatchFlags.TEXT) &&
            getGeneratedPropsConstantType(child, context) >=
              ConstantTypes.CAN_HOIST
          ) {
            // 獲取節點的 props,並在轉換上下文物件中執行提升操作,
            // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中
            const props = getNodeProps(child)
            if (props) {
              codegenNode.props = context.hoist(props)
            }
          }
          // 將節點的動態 props 新增到轉換上下文物件中
          if (codegenNode.dynamicProps) {
            codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
          }
        }
      }
    } else if (
      // 如果是節點型別是 TEXT_CALL,並且節點可以被提升
      child.type === NodeTypes.TEXT_CALL &&
      getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
    ) {
      // 提升節點
      child.codegenNode = context.hoist(child.codegenNode)
      hoistedCount++
    }
    // walk further
    if (child.type === NodeTypes.ELEMENT) {
      // 如果子節點的 tagType 是元件,則繼續遍歷子節點
      // 以判斷插槽中的情況
      const isComponent = child.tagType === ElementTypes.COMPONENT
      if (isComponent) {
        context.scopes.vSlot++
      }
      // 執行 walk函數,繼續判斷插槽中的節點及節點屬性是否可以被提升
      walk(child, context)
      if (isComponent) {
        context.scopes.vSlot--
      }
    } else if (child.type === NodeTypes.FOR) {
      // Do not hoist v-for single child because it has to be a block
      // 帶有 v-for 指令的節點是一個 Block
      // 如果 v-for 的節點中只有一個子節點,則不能被提升
      walk(child, context, child.children.length === 1)
    } else if (child.type === NodeTypes.IF) {
      // 帶有 v-if 指令的節點是一個 Block
      for (let i = 0; i < child.branches.length; i++) {
        // Do not hoist v-if single child because it has to be a block
        // 如果只有一個分支條件,則不進行提升
        walk(
          child.branches[i],
          context,
          child.branches[i].children.length === 1
        )
      }
    }
  }
  // 將被提升的節點序列化,即轉換成字串
  if (hoistedCount && context.transformHoist) {
    context.transformHoist(children, context, node)
  }
  // all children were hoisted - the entire children array is hoistable.
  if (
    hoistedCount &&
    hoistedCount === originalCount &&
    node.type === NodeTypes.ELEMENT &&
    node.tagType === ElementTypes.ELEMENT &&
    node.codegenNode &&
    node.codegenNode.type === NodeTypes.VNODE_CALL &&
    isArray(node.codegenNode.children)
  ) {
    node.codegenNode.children = context.hoist(
      createArrayExpression(node.codegenNode.children)
    )
  }
}

可以看到,walk 函數的程式碼比較常,下面,我們將分步對其進行解析。

1、首先,我們來看 walk 函數的函數簽名,程式碼如下:

function walk(
  node: ParentNode,
  context: TransformContext, // 轉換上下文物件
  doNotHoistNode: boolean = false  // 節點是否可以被提升
)

從函數簽名中可以知道,walk 函數接收三個引數,第一個引數是一個 node 節點,第二個引數是轉換器的轉換上下文物件 context,第三個引數 doNotHoistNode 是一個布林值,用來判斷傳入節點的子節點是否可以被提升。

2、初始化兩個變數,originalCount:子節點的數量;hoistedCount:可提升節點的數量,該變數將會用於判斷被提升的節點是否可序列化。

  const { children } = node
  // 子節點的數量
  const originalCount = children.length
  // 可提升節點的數量
  let hoistedCount = 0

3、我們來看 for 迴圈語句裡的第一個 if 語句分支,這裡處理的是普通元素和靜態文字被提升的情況。

    // only plain elements & text calls are eligible for hoisting.
    // 只有普通元素和文字才能被提升
    if (
      child.type === NodeTypes.ELEMENT &&
      child.tagType === ElementTypes.ELEMENT
    ) {
      // 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記
      // 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別
      const constantType = doNotHoistNode
        ? ConstantTypes.NOT_CONSTANT
        : getConstantType(child, context)
      // 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT
      if (constantType > ConstantTypes.NOT_CONSTANT) {
        // 如果節點可以被提升
        if (constantType >= ConstantTypes.CAN_HOIST) {
          // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升
          ;(child.codegenNode as VNodeCall).patchFlag =
            PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
            // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中
          child.codegenNode = context.hoist(child.codegenNode!)
          // 提升節點數量自增1
          hoistedCount++
          continue
        }
      } else {
        // node may contain dynamic children, but its props may be eligible for
        // hoisting.
        // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升
        const codegenNode = child.codegenNode!
        if (codegenNode.type === NodeTypes.VNODE_CALL) {
          // 獲取 patchFlag 修補程式標誌
          const flag = getPatchFlag(codegenNode)
          // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別
          // 並且該節點的 props 是可以被提升的
          if (
            (!flag ||
              flag === PatchFlags.NEED_PATCH ||
              flag === PatchFlags.TEXT) &&
            getGeneratedPropsConstantType(child, context) >=
              ConstantTypes.CAN_HOIST
          ) {
            // 獲取節點的 props,並在轉換上下文物件中執行提升操作,
            // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中
            const props = getNodeProps(child)
            if (props) {
              codegenNode.props = context.hoist(props)
            }
          }
          // 將節點的動態 props 新增到轉換上下文物件中
          if (codegenNode.dynamicProps) {
            codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
          }
        }
      }

首先通過外部傳入的 doNotHoistNode 引數來獲取子節點的靜態型別。如果 doNotHoistNode 為 true,則將 constantType 的值設定為 ConstantType 列舉值中的 NOT_CONSTANT,即不可被提升。否則通過 getConstantType 函數獲取子節點的靜態型別。如下面的程式碼所示:

// 如果節點不能被提升,則將 constantType 賦值為 NOT_CONSTANT 不可被提升的標記
// 否則呼叫 getConstantType 獲取子節點的靜態型別:ConstantTypes 定義了子節點的靜態型別
const constantType = doNotHoistNode
  ? ConstantTypes.NOT_CONSTANT
  : getConstantType(child, context)

接下來通過判斷 constantType 的列舉值來處理是需要提升靜態節點還是提升動態節點的靜態屬性。

  • 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT,說明該節點可能被提升或序列化為字串。
  • 如果該節點可以被提升,則將節點 codegenNode 屬性的 patchFlag 標記為 PatchFlags.HOISTED ,即可提升。

然後執行轉換器上下文中的 hoist 方法,將該節點儲存到轉換上下文context 的 hoist 陣列中,該陣列中儲存的是可被提升的節點。如下面的程式碼所示:

// 如果獲取到的 constantType 列舉值大於 NOT_CONSTANT
if (constantType > ConstantTypes.NOT_CONSTANT) {
  // 如果節點可以被提升
  if (constantType >= ConstantTypes.CAN_HOIST) {
    // 則將子節點的 codegenNode 屬性的 patchFlag 標記為 HOISTED ,即可提升
    ;(child.codegenNode as VNodeCall).patchFlag =
      PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
      // 提升節點,將節點儲存到 轉換上下文context 的 hoist 陣列中
    child.codegenNode = context.hoist(child.codegenNode!)
    // 提升節點數量自增1
    hoistedCount++
    continue
  }
}

如果獲取到的 constantType 列舉值不大於 NOT_CONSTANT,說明該節點包含動態繫結,包含動態繫結的節點上如果存在純靜態的 props,那麼這些靜態的 props 是可以被提升的

從下面的程式碼中我們可以看到,在提升靜態的 props 時,同樣是執行轉換器上下文中的 hoist 方法,將靜態的props儲存到轉換上下文context 的 hoist 陣列中。

如下面的程式碼所示:

else {
  // node may contain dynamic children, but its props may be eligible for
  // hoisting.
  // 包含動態繫結的節點本身不會被提升,該動態節點上可能存在純靜態的屬性,靜態的屬性可以被提升
  const codegenNode = child.codegenNode!
  if (codegenNode.type === NodeTypes.VNODE_CALL) {
    // 獲取 patchFlag 修補程式標誌
    const flag = getPatchFlag(codegenNode)
    // 如果不存在 patchFlag 修補程式標誌 或者 patchFlag 是文字型別
    // 並且該節點的 props 是可以被提升的
    if (
      (!flag ||
        flag === PatchFlags.NEED_PATCH ||
        flag === PatchFlags.TEXT) &&
      getGeneratedPropsConstantType(child, context) >=
        ConstantTypes.CAN_HOIST
    ) {
      // 獲取節點的 props,並在轉換上下文物件中執行提升操作,
      // 將被提升的 props 新增到轉換上下文context 的 hoist 陣列中
      const props = getNodeProps(child)
      if (props) {
        codegenNode.props = context.hoist(props)
      }
    }
    // 將節點的動態 props 新增到轉換上下文物件中
    if (codegenNode.dynamicProps) {
      codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
    }
  }
}

4、如果節點型別時是 TEXT_CALL 型別並且該節點可以被提升,則同樣執行轉換器上下文中的 hoist 方法,將節點儲存到轉換上下文context 的 hoist 陣列中,如下面的程式碼所示:

else if (
  // 如果是節點型別是 TEXT_CALL,並且節點可以被提升
  child.type === NodeTypes.TEXT_CALL &&
  getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
) {
  // 提升節點
  child.codegenNode = context.hoist(child.codegenNode)
  hoistedCount++
}

5、如果子節點是元件,則遞迴呼叫 walk 函數,繼續遍歷子節點,以判斷插槽中的節點及節點屬性是否可以被提升。如下面的程式碼所示:

if (child.type === NodeTypes.ELEMENT) {
  // 如果子節點的 tagType 是元件,則繼續遍歷子節點
  // 以判斷插槽中的情況
  const isComponent = child.tagType === ElementTypes.COMPONENT
  if (isComponent) {
    context.scopes.vSlot++
  }
  // 執行 walk函數,繼續判斷插槽中的節點及節點屬性是否可以被提升
  walk(child, context)
  if (isComponent) {
    context.scopes.vSlot--
  }
} 

6、如果節點上帶有 v-for 指令或 v-if 指令,則遞迴呼叫 walk 函數,繼續判斷子節點是否可以被提升。如果 v-for 指令的節點只有一個子節點,v-if 指令的節點只有一個分支條件,則不進行提升。如下面的程式碼所示:

else if (child.type === NodeTypes.FOR) {
  // Do not hoist v-for single child because it has to be a block
  // 帶有 v-for 指令的節點是一個 Block
  // 如果 v-for 的節點中只有一個子節點,則不能被提升
  walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
  // 帶有 v-if 指令的節點是一個 Block
  for (let i = 0; i < child.branches.length; i++) {
    // Do not hoist v-if single child because it has to be a block
    // 如果只有一個分支條件,則不進行提升
    walk(
      child.branches[i],
      context,
      child.branches[i].children.length === 1
    )
  }
}

walk 函數流程圖

總結

靜態提升,就是指在編譯器編譯的過程中,將一些靜態的節點或屬性提升到渲染函數之外。它能夠減少更新時建立虛擬 DOM 帶來的效能開銷和記憶體佔用。

對於純靜態的節點和動態節點上的純靜態屬性,則直接執行轉換器上下文中的 hoist 方法,將節點或屬性進行提升。如果節點是元件、節點上帶有 v-for 指令或v-if 指令,則遞迴呼叫 walk 函數判斷子節點是否可以被提升。

以上就是Vue3 原始碼解讀靜態提升詳解的詳細內容,更多關於Vue3 靜態提升的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com