Vue3 源碼解讀之 Teleport 組件使用示例

Teleport 組件解決的問題

版本:3.2.31

如果要實現一個 “蒙層” 的功能,並且該 “蒙層” 可以遮擋頁面上的所有元素,通常情況下我們會選擇直接在 標簽下渲染 “蒙層” 內容。如果在Vue.js 2 中實現這個功能,隻能通過原生 DOM API 來手動搬運 DOM元素實現,這就會使得元素的渲染與 Vue.js 的渲染機制脫節,並會導致各種可預見或不可遇見的問題。

Vue.js 3 中內建的 Teleport 組件,可以將指定內容渲染到特定容器中,而不受DOM層級的限制。可以很好的解決這個問題。

下面,我們來看看 Teleport 組件是如何解決這個問題的。如下是基於 Teleport 組件實現的蒙層組件的模板:

<template>
  <Teleport to="body">
    <div class="overlay"></div>
  </Teleport>
</template>
<style scoped>
  .verlay {
    z-index: 9999;
  }
</style>

可以看到,蒙層組件要渲染的內容都包含在 Teleport 組件內,即作為 Teleport 組件的插槽。

通過為 Teleport 組件指定渲染目標 body,即 to 屬性的值,該組件就會把它的插槽內容渲染到 body 下,而不會按照模板的 DOM 層級來渲染,於是就實現瞭跨 DOM 層級的渲染。

從而實現瞭蒙層可以遮擋頁面中的所有內容。

Teleport 組件的基本結構

// packages/runtime-core/src/components/Teleport.ts
export const TeleportImpl = {
  // Teleport 組件獨有的特性,用作標識
  __isTeleport: true,
  // 客戶端渲染 Teleport 組件
  process() {},
  // 移除 Teleport
  remove() {},
  //  移動 Teleport
  move: moveTeleport,
  // 服務端渲染 Teleport
  hydrate: hydrateTeleport
}
export const Teleport = TeleportImpl as any as {
  __isTeleport: true
  new (): { $props: VNodeProps & TeleportProps }
}

我們對 Teleport 組件的源碼做瞭精簡,如上面的代碼所示,可以看到,一個組件就是一個選項對象。Teleport 組件上有 __isTeleport、process、remove、move、hydrate 等屬性。其中 __isTeleport 屬性是 Teleport 組件獨有的特性,用作標識。process 函數是渲染 Teleport 組件的主要渲染邏輯,它從渲染器中分離出來,可以避免渲染器邏輯代碼 “膨脹”。

Teleport 組件 process 函數

process 函數主要用於在客戶端渲染 Teleport 組件。由於 Teleport 組件需要渲染器的底層支持,因此將 Teleport 組件的渲染邏輯從渲染器中分離出來,在 Teleport 組件中實現其渲染邏輯。這麼做有以下兩點好處:

  • 可以避免渲染器邏輯代碼 “膨脹”;
  • 當用戶沒有使用 Teleport 組件時,由於 Teleport 的渲染邏輯被分離,因此可以利用 Tree-Shaking 機制在最終的 bundle 中刪除 Teleport 相關的代碼,使得最終構建包的體積變小。

patch 函數中對 process 函數的調用如下:

// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略部分代碼
    const { type, ref, shapeFlag } = n2
    switch (type) {
      // 省略部分代碼
      default:
        // 省略部分代碼
        // shapeFlag 的類型為 TELEPORT,則它是 Teleport 組件
        // 調用 Teleport 組件選項中的 process 函數將控制權交接出去
        // 傳遞給 process 函數的第五個參數是渲染器的一些內部方法
        else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
        // 省略部分代碼
    }
    // 省略部分代碼
  }

從上面的源碼中可以看到,我們通過vnode 的 shapeFlag 來判斷組件是否是 Teleport 組件。如果是,則直接調用組件選項中定義的 process 函數將渲染控制權完全交接出去,這樣就實現瞭渲染邏輯的分離。

Teleport 組件的掛載

// packages/runtime-core/src/components/Teleport.ts
if (n1 == null) {
  // 首次渲染 Teleport
  // insert anchors in the main view
  // 往 container 中插入 Teleport 的註釋
  const placeholder = (n2.el = __DEV__
    ? createComment('teleport start')
    : createText(''))
  const mainAnchor = (n2.anchor = __DEV__
    ? createComment('teleport end')
    : createText(''))
  insert(placeholder, container, anchor)
  insert(mainAnchor, container, anchor)
  // 獲取容器,即掛載點
  const target = (n2.target = resolveTarget(n2.props, querySelector))
  const targetAnchor = (n2.targetAnchor = createText(''))
  // 如果掛載點存在,則將
  if (target) {
    insert(targetAnchor, target)
    // #2652 we could be teleporting from a non-SVG tree into an SVG tree
    isSVG = isSVG || isTargetSVG(target)
  } else if (__DEV__ && !disabled) {
    warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
  }
  // 將 n2.children 渲染到指定掛載點
  const mount = (container: RendererElement, anchor: RendererNode) => {
    // Teleport *always* has Array children. This is enforced in both the
    // compiler and vnode children normalization.
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 調用渲染器內部的 mountChildren 方法渲染 Teleport 組件的插槽內容
      mountChildren(
        children as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
  // 掛載 Teleport
  if (disabled) {
    // 如果 Teleport 組件的 disabled 為 true,說明禁用瞭 <teleport> 的功能,Teleport 隻會在 container 中渲染
    mount(container, mainAnchor)
  } else if (target) {
    // 如果沒有禁用 <teleport> 的功能,並且存在掛載點,則將其插槽內容渲染到target容中
    mount(target, targetAnchor)
  }
}

從上面的源碼中可以看到,如果舊的虛擬節點 (n1) 不存在,則執行 Teleport 組件的掛載。然後調用 resolveTarget 函數,根據 props.to 屬性的值來取得真正的掛載點。

如果沒有禁用 的功能 (disabled 為 false ),則調用渲染器內部的 mountChildren 方法將 Teleport 組件掛載到目標元素中。如果 的功能被禁用,則 Teleport 組件將會在周圍父組件中指定瞭 的位置渲染。

Teleport 組件的更新

Teleport 組件在更新時需要考慮多種情況,如下面的代碼所示:

// packages/runtime-core/src/components/Teleport.ts
else {
  // 更新 Teleport 組件
  // update content
  n2.el = n1.el
  const mainAnchor = (n2.anchor = n1.anchor)!
  // 掛載點
  const target = (n2.target = n1.target)!
  // 錨點
  const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  // 判斷 Teleport 組件是否禁用瞭 
  const wasDisabled = isTeleportDisabled(n1.props)
  // 如果禁用瞭 <teleport> 的功能,那麼掛載點就是周圍父組件,否則就是 to 指定的目標掛載點
  const currentContainer = wasDisabled ? container : target
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  // 目標掛載點是否是 SVG 標簽元素
  isSVG = isSVG || isTargetSVG(target)
  // 動態子節點的更新
  if (dynamicChildren) {
    // fast path when the teleport happens to be a block root
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      currentContainer,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds
    )
    // even in block tree mode we need to make sure all root-level nodes
    // in the teleport inherit previous DOM references so that they can
    // be moved in future patches.
    // 確保所有根級節點在移動之前可以繼承之前的 DOM 引用,以便它們在未來的補丁中移動
    traverseStaticChildren(n1, n2, true)
  } else if (!optimized) {
    // 更新子節點
    patchChildren(
      n1,
      n2,
      currentContainer,
      currentAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      false
    )
  }
  // 如果禁用瞭 <teleport> 的功能
  if (disabled) {
    if (!wasDisabled) {
      // enabled -> disabled
      // move into main container
      // 將 Teleport 移動到container容器中
      moveTeleport(
        n2,
        container,
        mainAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    }
  } else {
    // 沒有禁用 <teleport> 的功能,判斷 to 是否發生變化
    // target changed
    // 如果新舊 to 的值不同,則需要對內容進行移動
    if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
      // 獲取新的目標容器
      const nextTarget = (n2.target = resolveTarget(
        n2.props,
        querySelector
      ))
      if (nextTarget) {
        // 移動到新的容器中
        moveTeleport(
          n2,
          nextTarget,
          null,
          internals,
          TeleportMoveTypes.TARGET_CHANGE
        )
      } else if (__DEV__) {
        warn(
          'Invalid Teleport target on update:',
          target,
          `(${typeof target})`
        )
      }
    } else if (wasDisabled) {
      // disabled -> enabled
      // move into teleport target
      // 
      moveTeleport(
        n2,
        target,
        targetAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    }
  }
}

如果 Teleport 組件的子節點中有動態子節點,則調用 patchBlockChildren 函數來更新子節點,否則就調用 patchChildren 函數來更新子節點。

接下來判斷 Teleport 的功能是否被禁用。如果被禁用瞭,即 Teleport 組件的 disabled 屬性為 true,此時 Teleport 組件隻會在周圍父組件中指定瞭 的位置渲染。

如果沒有被禁用,那麼需要判斷 Teleport 組件的 to 屬性值是否發生變化。如果發生變化,則需要獲取新的掛載點,然後調用 moveTeleport 函數將Teleport組件掛載到到新的掛載點中。如果沒有發生變化,則 Teleport 組件將會掛載到先的掛載點中。

moveTeleport 移動Teleport 組件

// packages/runtime-core/src/components/Teleport.ts
function moveTeleport(
  vnode: VNode,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
  // move target anchor if this is a target change.
  // 插入到目標容器中
  if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
    insert(vnode.targetAnchor!, container, parentAnchor)
  }
  const { el, anchor, shapeFlag, children, props } = vnode
  const isReorder = moveType === TeleportMoveTypes.REORDER
  // move main view anchor if this is a re-order.
  if (isReorder) {
    // 插入到目標容器中
    insert(el!, container, parentAnchor)
  }
  // if this is a re-order and teleport is enabled (content is in target)
  // do not move children. So the opposite is: only move children if this
  // is not a reorder, or the teleport is disabled
  if (!isReorder || isTeleportDisabled(props)) {
    // Teleport has either Array children or no children.
    if (shapeFlag &amp; ShapeFlags.ARRAY_CHILDREN) {
      // 遍歷子節點
      for (let i = 0; i &lt; (children as VNode[]).length; i++) {
        // 調用 渲染器的黑佈方法 move將子節點移動到目標元素中
        move(
          (children as VNode[])[i],
          container,
          parentAnchor,
          MoveType.REORDER
        )
      }
    }
  }
  // move main view anchor if this is a re-order.
  if (isReorder) {
    // 插入到目標容器中
    insert(anchor!, container, parentAnchor)
  }
}

從上面的源碼中可以看到,將 Teleport 組件移動到目標掛載點中,實際上就是調用渲染器的內部方法 insert 和 move 來實現子節點的插入和移動。

hydrateTeleport 服務端渲染 Teleport 組件

hydrateTeleport 函數用於在服務器端渲染 Teleport 組件,其源碼如下:

// packages/runtime-core/src/components/Teleport.ts
// 服務端渲染 Teleport
function hydrateTeleport(
  node: Node,
  vnode: TeleportVNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  slotScopeIds: string[] | null,
  optimized: boolean,
  {
    o: { nextSibling, parentNode, querySelector }
  }: RendererInternals<Node, Element>,
  hydrateChildren: (
    node: Node | null,
    vnode: VNode,
    container: Element,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => Node | null
): Node | null {
  // 獲取掛載點
  const target = (vnode.target = resolveTarget<Element>(
    vnode.props,
    querySelector
  ))
  if (target) {
    // if multiple teleports rendered to the same target element, we need to
    // pick up from where the last teleport finished instead of the first node
    const targetNode =
      (target as TeleportTargetElement)._lpa || target.firstChild
    if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // <teleport> 的功能被禁用,將 Teleport 渲染到父組件中指定瞭 <teleport> 的位置
      if (isTeleportDisabled(vnode.props)) {
        vnode.anchor = hydrateChildren(
          nextSibling(node),
          vnode,
          parentNode(node)!,
          parentComponent,
          parentSuspense,
          slotScopeIds,
          optimized
        )
        vnode.targetAnchor = targetNode
      } else {
        vnode.anchor = nextSibling(node)
        // 將 Teleport 渲染到目標容器中
        vnode.targetAnchor = hydrateChildren(
          targetNode,
          vnode,
          target,
          parentComponent,
          parentSuspense,
          slotScopeIds,
          optimized
        )
      }
      ;(target as TeleportTargetElement)._lpa =
        vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
    }
  }
  return vnode.anchor && nextSibling(vnode.anchor as Node)
}

可以看到,在服務端渲染 Teleport 組件時,調用的是服務端渲染的 hydrateChildren 函數來渲染Teleport的內容。如果 的功能被禁用,將 Teleport 渲染到父組件中指定瞭 的位置,否則將 Teleport 渲染到目標容器target中。

總結

本文介紹瞭 Teleport 組件索要解決的問題和它的實現原理。Teleport 組件可以跨越 DOM 層級完成渲染。在實現 Teleport 組件時,將 Teleport 組件的渲染邏輯 (即 Teleport 組件的 process 函數) 從渲染器中分離出來,是為瞭避免渲染器邏輯代碼 “膨脹” 以及可以利用 Tree-Shaking 機制在最終的 bundle 中刪除 Teleport 相關的代碼,使得最終構建包的體積變小。

Teleport 組件在掛載時會根據 的功能是否禁用從而將其掛載到相應的掛載點中。在更新時同樣會根據 的功能是否被禁用以及 to 屬性值是否發生變化,從而將其移動到相應的掛載點中。

以上就是Vue3 源碼解讀之 Teleport 組件使用示例的詳細內容,更多關於Vue3 Teleport組件的資料請關註WalkonNet其它相關文章!

推薦閱讀: