Skip to content

Teleport 是 Vue 3 中一个极其巧妙的内置组件,它允许你将一段模板渲染的 DOM “传送” 到 DOM 树的任意位置,而逻辑上它仍然是当前组件的子组件。这在实现模态框(Modal)、全局通知、Tooltip 等需要脱离当前布局的 UI 元素时非常有用。

其核心在于 move() 操作,这是 Vue 渲染器内部用于动态迁移 VNode 对应的 DOM 节点的关键机制。


一、Teleport 的基本用法

vue
<template>
  <div class="component">
    <h1>这是组件内部</h1>
    
    <!-- 使用 Teleport 将内容传送到 body 下 -->
    <Teleport to="body">
      <div class="modal">
        <p>这是一个模态框,但它在 body 下</p>
      </div>
    </Teleport>
  </div>
</template>

渲染后,.modal 元素会出现在 <body> 的末尾,而不是 .component 内部。


二、实现原理:VNode 与 DOM 的分离

Teleport 的魔力在于它解耦了 VNode 的逻辑位置和 DOM 的物理位置

1. 逻辑结构 vs 物理结构

  • 逻辑结构(组件树):

    Component
    └── Teleport
        └── div.modal

    div.modal 在组件树中是 Component 的后代。

  • 物理结构(DOM 树):

    body
    └── div.modal
    
    div.component
    └── (Teleport 占位符)

    div.modal 的实际 DOM 节点被移动到了 <body> 下。

Teleport 在原始位置留下一个占位符节点(comment node),而将真实内容挂载到目标容器。


三、核心机制:move() 操作

move() 是 Vue 渲染器(Renderer)内部的一个关键函数,定义在 renderer.ts 中。它的作用是将一个已存在的 DOM 节点(或节点序列)从当前位置移动到另一个位置

1. move() 函数签名(简化)

ts
function move(
  vnode: VNode,
  container: RendererElement, // 目标容器
  anchor: RendererNode | null, // 插入位置的参考节点
  type: MoveType = MoveType.REORDER
) {
  // 获取 VNode 对应的真实 DOM 节点
  const el = vnode.el;
  
  if (el) {
    // 使用原生 DOM API 移动节点
    insert(el, container, anchor);
  }
}
  • insert 内部使用 container.insertBefore(el, anchor)container.appendChild(el)

2. Teleport 如何触发 move()

Teleport 组件的实现逻辑如下:

(1) 挂载阶段(Mount)
  • 创建目标容器:根据 to 属性(如 "body")找到或创建目标 DOM 容器。
  • 渲染子节点Teleport#default 插槽内容会被正常渲染成 VNode。
  • 执行 move()
    • 渲染器调用 move(),将子 VNode 对应的 DOM 节点从当前父容器移动到目标容器。
    • Teleport 的原始位置插入一个注释节点作为占位符(如 <!--teleport start-->...<!--teleport end-->)。
(2) 更新阶段(Update)
  • 目标容器变化:如果 to 属性动态改变(如从 "body" 变为 "#modal-container"),Teleport 会:
    1. 在新的目标容器中创建 DOM(如果需要)。
    2. 再次调用 move(),将 DOM 节点从旧目标移动到新目标。
(3) 卸载阶段(Unmount)
  • 清理:当 Teleport 组件被卸载时,它会:
    1. 从目标容器中移除其内容的 DOM 节点。
    2. 移除原始位置的占位符注释节点。

四、move() 操作的细节

1. 不是“重新渲染”,而是“移动”

  • move() 不重新创建 DOM 节点,只是改变其在 DOM 树中的位置。
  • 这意味着:
    • 事件监听器保留:绑定的事件不会丢失。
    • 组件状态保留:内部组件的 dataref 等状态不变。
    • 性能高效:避免了重新渲染的开销。

2. 支持 Fragment 和多节点

Teleport 可以传送多个子节点(Fragment):

vue
<Teleport to="body">
  <div>Node 1</div>
  <div>Node 2</div>
</Teleport>

move() 会处理整个 VNode 子树,将所有对应 DOM 节点作为一个整体移动。

3. 动态 to 目标

vue
<Teleport :to="dynamicTarget">
  <div>动态传送</div>
</Teleport>

dynamicTarget 变化时,Teleport 的更新逻辑会:

  1. 比较新旧 to 值。
  2. 如果不同,则调用 move() 将内容迁移到新容器。

这完全由 move() 操作实现。


五、与 appendChild 的区别

你可能会想:“为什么不直接用 document.body.appendChild()?”

  • 手动操作 DOMappendChild 是命令式的,破坏了 Vue 的声明式理念。
  • 状态管理困难:你需要手动管理节点的创建、移动、销毁,容易出错。
  • 与响应式系统脱节:无法自动响应 v-ifv-for 等指令的变化。

Teleport 通过 move() 将 DOM 移动集成到 Vue 的渲染管线中,使其完全响应式和声明式。


六、源码层面的关键点

在 Vue 3 源码中,Teleport 的实现位于 runtime-core/components/Teleport.ts

核心逻辑摘要

ts
const processTeleport = (
  n1: TeleportVNode | null,
  n2: TeleportVNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  const target = resolveTarget(n2.props, querySelector);
  
  if (n1) {
    // 更新
    const currentContainer = (n1 as any).depatchTo;
    const currentAnchor = (n1 as any).depatchAnchor;
    
    if (target === currentContainer) {
      // 目标未变,只需移动 anchor
      move(n2.children, container, anchor, MoveType.REORDER);
    } else {
      // 目标变了,需要迁移到新容器
      move(n2.children, target, null, MoveType.TELEPORT);
    }
  } else {
    // 首次挂载
    hostInsert(anchor, container, anchor);
    // 将子节点移动到目标
    move(n2.children, target, null, MoveType.TELEPORT);
  }
}
  • move() 是核心,MoveType.TELEPORT 标记了这是由 Teleport 触发的移动。

七、使用场景与最佳实践

1. 常见场景

  • 模态框/对话框:避免被父容器的 overflow: hidden 截断。
  • 全局通知/Toast:统一挂载到 body,避免布局干扰。
  • Tooltip/Popover:渲染到 body 以脱离文档流,方便定位。
  • 全屏覆盖层:确保覆盖整个视口。

2. 注意事项

  • 样式隔离:传送出去的 DOM 不再受父组件 CSS 作用域影响。
  • z-index 管理:需要合理设置 z-index 避免层级问题。
  • 可访问性:确保模态框等元素的 aria 属性正确。

八、总结

Teleport 的实现原理可以归结为:

  1. 占位与迁移:在逻辑位置留下占位符,在物理位置渲染真实 DOM。
  2. move() 操作:通过渲染器的 move() 函数,将 VNode 对应的 DOM 节点高效地移动到目标容器。
  3. 声明式控制to 属性驱动整个过程,完全响应式。

move()Teleport 的“引擎”,它使得 DOM 节点可以在不丢失状态的情况下动态迁移,这是 Vue 实现“逻辑与物理分离”的关键技术。理解 move(),你就理解了 Teleport 的灵魂。