Appearance
Teleport:将内容渲染到任意 DOM 节点
引言
在组件开发中,有时会遇到“层级限制”问题。例如,一个“模态框”(Modal)组件,在逻辑上属于某个按钮,但视觉上,它必须被渲染到 document.body 的顶层,才能绕过父组件的 overflow: hidden 或 z-index 限制。
<Teleport>(传送)是 Vue 3 为解决这一问题而生的内置组件。
核心机制:“逻辑锚点”与“物理锚点”
理解 Teleport 的关键在于它同时维护着两个位置:
逻辑位置 (Logical Location):
<Teleport>标签在组件模板中所在的位置。Vue 的 VNode Diff 算法会在这里“找到”它。它由一对“占位锚点”(placeholder和mainAnchor)来标记。物理位置 (Physical Location): 由
to属性(如to="body")指定的真实 DOM 容器。组件的子节点(children)实际被渲染的地方。它由另一对“目标锚点”(targetStart和targetAnchor)来标记。
Teleport 的工作机制,就是拦截(Intercept)渲染器的 patch 和 unmount 操作,将本应在“逻辑位置”执行的 DOM 操作,“重定向”(Redirect)到“物理位置”去执行。
核心实现:TeleportImpl
<Teleport> 组件的实现是一个名为 TeleportImpl 的对象。它没有 setup 或 template,只有一个 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。
process 的 else 分支(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 渲染器中一个“重定向”机制:
- “逻辑”与“物理”分离:它在原位(逻辑)保留“占位锚点”,在目标(物理)创建“内容锚点”。
patch拦截:它的process函数拦截patch流程。- 挂载重定向:
mount时,mountChildren被重定向到“物理位置”。 - 更新重定向:
update时,patchChildren被重定向到正确的位置(取决于disabled状态)。 - DOM 移动:
moveTeleport负责在“两个位置”之间物理移动 DOM 节点。 - 卸载:
remove负责同时清理“两个位置”的锚点和内容。
