Skip to content

Teleport:将内容渲染到任意 DOM 节点

引言

在组件开发中,有时会遇到“层级限制”问题。例如,一个“模态框”(Modal)组件,在逻辑上属于某个按钮,但视觉上,它必须被渲染到 document.body 的顶层,才能绕过父组件的 overflow: hiddenz-index 限制。

<Teleport>(传送)是 Vue 3 为解决这一问题而生的内置组件。

核心机制:“逻辑锚点”与“物理锚点”

理解 Teleport 的关键在于它同时维护着两个位置

  1. 逻辑位置 (Logical Location)<Teleport> 标签在组件模板中所在的位置。Vue 的 VNode Diff 算法会在这里“找到”它。它由一对“占位锚点”(placeholdermainAnchor)来标记。

  2. 物理位置 (Physical Location): 由 to 属性(如 to="body")指定的真实 DOM 容器。组件的子节点children实际被渲染的地方。它由另一对“目标锚点”(targetStarttargetAnchor)来标记。

Teleport 的工作机制,就是拦截(Intercept)渲染器的 patchunmount 操作,将本应在“逻辑位置”执行的 DOM 操作,“重定向”(Redirect)到“物理位置”去执行。


核心实现:TeleportImpl

<Teleport> 组件的实现是一个名为 TeleportImpl 的对象。它没有 setuptemplate,只有一个 process 函数。这个 process 函数会在 patch 流程中被调用,充当“拦截器”。

typescript
// core/packages/runtime-core/src/components/Teleport.ts
export const TeleportImpl = {
  name: 'Teleport',
  __isTeleport: true,
  
  // n1 = old VNode, n2 = new VNode
  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement, // “逻辑位置”的容器
    anchor: RendererNode | null, // “逻辑位置”的锚点
    parentComponent: ComponentInternalInstance | null,
    // ...
    internals: RendererInternals, // 包含 { insert, move, remove } 等 DOM 操作
  ): void {
    
    if (n1 == null) {
      // --- 挂载 (Mount) 流程 ---
      // 1. 在“逻辑位置”创建占位符
      // 2. 找到“物理位置”
      // 3. 在“物理位置”挂载真实内容
    } else {
      // --- 更新 (Update) 流程 ---
      // 1. 检查 'disabled' 或 'to' 是否变化
      // 2. 决定是在“逻辑位置”还是“物理位置”更新子节点
      // 3. (如有必要) 移动 DOM
    }
  }
}

挂载 (n1 == null):建立两个位置

当 Teleport 首次挂载时,process 函数的任务是同时在两个位置建立锚点。

typescript
// TeleportImpl.process 的 n1 == null 分支 (简化)

// 1. 【建立“逻辑位置”】
// 在组件模板的“原位”插入两个注释节点作为“占位符”
const placeholder = (n2.el = __DEV__
  ? createComment('teleport start')
  : createText(''))
const mainAnchor = (n2.anchor = __DEV__
  ? createComment('teleport end')
  : createText(''))

// insert 是底层的 DOM 操作
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)

// 2. 定义“挂载内容”的函数
const mount = (container: RendererElement, anchor: RendererNode) => {
  // 这是真正挂载 <Teleport> 子节点的逻辑
  mountChildren(
    children as VNodeArrayChildren,
    container, // 目标容器 (可能是“物理位置”)
    anchor,    // 目标锚点 (可能是“物理位置”)
    ...
  )
}

// 3. 检查 'disabled' 状态
const disabled = isTeleportDisabled(n2.props)

if (disabled) {
  // 3a. 如果禁用,内容就挂载在“逻辑位置”
  mount(container, mainAnchor)
} else {
  // 3b. 如果启用,内容挂载到“物理位置”
  mountToTarget()
}

// 4. 定义“挂载到物理位置”的函数
const mountToTarget = () => {
  // 4a. 解析 'to' 属性,找到目标 DOM
  const target = (n2.target = resolveTarget(n2.props, querySelector))
  
  if (target) {
    // 4b. 【建立“物理位置”】
    //    在“物理位置” (target) 内部也创建一对锚点
    const targetAnchor = prepareAnchor(target, n2, createText, insert)
    
    // 4c. 【传送】
    //    调用 mount 函数,将子节点挂载到 target
    mount(target, targetAnchor)
  }
}

prepareAnchor 的工作是在 target 容器的末尾插入“目标锚点”:

typescript
function prepareAnchor(target: RendererElement, vnode: TeleportVNode, ...) {
  // targetStart 和 targetAnchor 也是注释节点
  const targetStart = (vnode.targetStart = createText(''))
  const targetAnchor = (vnode.targetAnchor = createText(''))

  if (target) {
    insert(targetStart, target)
    insert(targetAnchor, target)
  }
  return targetAnchor
}

挂载总结Teleport 在“逻辑位置”留下了一对的 `` 锚点,同时在“物理位置”(如 body)中创建了另一对锚点,并将真实内容(`children`)渲染到了这对锚点之间。


更新 (n1 != null):重定向 patch**

当组件更新时,Vue 的 patch 算法会找到 Teleport 在“逻辑位置”的 VNode 并执行 process

processelse 分支(n1 != null)负责处理更新:

typescript
// TeleportImpl.process 的 else 分支 (简化)
const wasDisabled = isTeleportDisabled(n1.props)
const disabled = isTeleportDisabled(n2.props)
const target = n2.target // 物理位置
const container = n1.el!.parentNode // 逻辑位置

// 1. 【决定在哪里 patch】
//    根据“上一次”的 disabled 状态,
//    决定子节点是在“物理位置”还是“逻辑位置”被 patch
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor

// 2. 【重定向 Patch】
//    无论 patch 在 VNode 树的哪里被触发,
//    patchChildren 都会被“重定向”到正确的容器
patchChildren(
  n1,
  n2,
  currentContainer, // 可能是 target,也可能是 container
  currentAnchor,
  ...
)

// 3. 【处理状态切换】
if (disabled) {
  if (!wasDisabled) {
    // 状态从“启用” -> “禁用”
    // 【传送回来】:把内容从“物理位置”移回“逻辑位置”
    moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE)
  }
} else {
  // 状态是“启用”
  if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
    // 1. 如果 'to' 目标变了
    // 【切换目标】:移动到新的“物理位置”
    const nextTarget = resolveTarget(n2.props, querySelector)
    moveTeleport(n2, nextTarget, null, internals, TeleportMoveTypes.TARGET_CHANGE)
  } else if (wasDisabled) {
    // 2. 状态从“禁用” -> “启用”
    // 【传送过去】:把内容从“逻辑位置”移到“物理位置”
    moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
  }
}

moveTeleport:DOM 移动

moveTeleport 函数负责物理 DOM 节点的移动。它会遍历 Teleport 的所有子节点(children),并调用底层的 move (即 node.parentNode.insertBefore)。

typescript
function moveTeleport(
  vnode: VNode,
  container: RendererElement, // 新的容器
  parentAnchor: RendererNode | null, // 新的锚点
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes
): void {
  const { el, anchor, shapeFlag, children, props } = vnode

  // ... (省略锚点自身的移动) ...

  // 1. 如果是“禁用”状态,才需要移动“逻辑位置”的占位符
  if (isReorder || isTeleportDisabled(props)) {
    insert(el!, container, parentAnchor)
  }
  
  // 2. 【核心】移动所有子节点
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    for (let i = 0; i < (children as VNode[]).length; i++) {
      // "move" 是底层的 DOM 移动操作
      move(
        (children as VNode[])[i], // 每一个子 VNode
        container, // 移动到新容器
        parentAnchor,
        MoveType.REORDER,
      )
    }
  }

  // 3. 移动“逻辑位置”的结束锚点
  if (isReorder || isTeleportDisabled(props)) {
    insert(anchor!, container, parentAnchor)
  }
}

卸载 (remove):清理两个位置

当 Teleport 组件被销毁时(例如 v-if="false"),它的 remove 方法被调用。它必须负责清理两个位置。

typescript
// TeleportImpl.remove
remove(
  vnode: VNode,
  // ...
  { um: unmount, o: { remove: hostRemove } }: RendererInternals,
  doRemove: boolean,
): void {
  const { shapeFlag, children, anchor, target, targetStart, targetAnchor, props } = vnode

  // 1. 【清理“物理位置”】
  //    如果 target 存在,移除在“物理位置”创建的锚点
  if (target) {
    hostRemove(targetStart!)
    hostRemove(targetAnchor!)
  }

  // 2. 【清理“逻辑位置”】
  //    如果需要(doRemove),移除在“逻辑位置”的占位锚点
  doRemove && hostRemove(anchor!)
  
  // 3. 【卸载子节点】
  //    'doRemove' 只有在 teleport 'disabled' 时才为 false
  const shouldRemove = doRemove || !isTeleportDisabled(props)
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    for (let i = 0; i < (children as VNode[]).length; i++) {
      // 递归卸载所有子 VNode
      // 'shouldRemove' 决定了是否真的移除 DOM 
      // (如果已传送,则不移除 DOM,只卸载组件实例)
      unmount(children[i], ..., shouldRemove, ...)
    }
  }
}

总结

<Teleport> 的实现是 Vue 渲染器中一个“重定向”机制:

  1. “逻辑”与“物理”分离:它在原位(逻辑)保留“占位锚点”,在目标(物理)创建“内容锚点”。
  2. patch 拦截:它的 process 函数拦截 patch 流程。
  3. 挂载重定向mount 时,mountChildren 被重定向到“物理位置”。
  4. 更新重定向update 时,patchChildren 被重定向到正确的位置(取决于 disabled 状态)。
  5. DOM 移动moveTeleport 负责在“两个位置”之间物理移动 DOM 节点。
  6. 卸载remove 负责同时清理“两个位置”的锚点和内容。

微信公众号二维码

Last updated: