首頁 > 軟體

Vue3 原始碼解讀之 Teleport 元件使用範例

2022-08-19 14:01:12

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元件的資料請關注it145.com其它相關文章!


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