Teleport 用来将组件内容渲染到当前组件树之外的 DOM 容器中。
2025-11-23 16:04:26
Teleport 用来将组件内容渲染到当前组件树之外的 DOM 容器中。
常用于模态框、弹出层等场景:组件逻辑仍然属于当前组件,但真实 DOM 可以挂载到其他位置。
<div id="app"></div>
<div id="modal"></div>
import { createApp, h, Teleport } from '../dist/vue.esm.js'
const App = {
render() {
return h('div', [
h('p', 'app content'),
h(
Teleport,
{ to: '#modal' },
[
h('div', 'modal content')
]
)
])
}
}
createApp(App).mount('#app')
modal content 不会插入到 #app 内部,而是会插入到 #modal 中。
渲染流程
实现主要分为四步:
- 创建
vnode时识别Teleport,并设置ShapeFlags.TELEPORT patch过程中将 Teleport 分发给它自己的process方法- 挂载或更新时,根据
to和disabled决定子节点的容器 - 卸载时,直接卸载 Teleport 的子节点
实现
1. 识别为 Teleport
// packages/runtime-core/src/components/Teleport.ts
export const Teleport = {
name: 'Teleport',
__isTeleport: true,
props: {
to: { type: String },
disabled: { type: Boolean }
}
}
__isTeleport: 识别 Teleport 标记。
Teleport 核心属性:
to:目标容器的选择器disabled:是否禁用。禁用时,子节点会渲染到当前容器
在 createVNode 中,根据 type 设置对应的 shapeFlag:
export function createVNode(type, props?, children = null) {
let shapeFlag = 0
if (isString(type)) {
shapeFlag = ShapeFlags.ELEMENT
}
else if (isTeleport(type)) {
shapeFlag = ShapeFlags.TELEPORT
}
else if (isObject(type)) {
shapeFlag = ShapeFlags.STATEFUL_COMPONENT
}
else if (isFunction(type)) {
shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
}
const vnode = {
type,
props,
children,
shapeFlag
// ...
}
return vnode
}
当 type 是 Teleport 时,vnode 的 shapeFlag 就是 ShapeFlags.TELEPORT。
2. 分发 Teleport
function patch(n1, n2, container, anchor = null, parentComponent = null) {
// ...
const { shapeFlag, type } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent)
}
else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent)
}
else if (shapeFlag & ShapeFlags.TELEPORT) {
type.process(n1, n2, container, anchor, parentComponent, {
mountChildren,
patchChildren,
options
})
}
}
}
Teleport 直接调用自身的 process 方法,将 mountChildren、patchChildren 和平台相关的 options 传递。
这样 Teleport 只负责决定渲染位置,具体的节点挂载和更新仍然复用渲染器已有的逻辑。
3. 挂载和更新 Teleport
export const isTeleport = type => type.__isTeleport
export const Teleport = {
name: 'Teleport',
__isTeleport: true,
props: {
to: {
type: String
},
disabled: {
type: Boolean
}
},
process(n1, n2, container, anchor, parentComponent, internals) {
const {
mountChildren,
patchChildren,
options: { querySelector, insert }
} = internals
const { to, disabled } = n2.props
if (n1 == null) {
const target = disabled ? container : querySelector(to)
if (target) {
n2.target = target
mountChildren(target, n2.children, parentComponent)
}
return
}
// 先在旧容器中更新子节点
patchChildren(n1, n2, n1.target, parentComponent)
n2.target = n1.target
const prevProps = n1.props || {}
// to 或 disabled 发生变化时,移动子节点
if (prevProps.to !== to || prevProps.disabled !== disabled) {
const target = disabled ? container : querySelector(to)
if (target) {
for (const child of n2.children) {
insert(child.el, target)
}
n2.target = target
}
}
}
}
挂载阶段的处理比较直接:
disabled为true时,使用当前的containerdisabled为false时,通过querySelector(to)查找目标容器- 找到目标容器后,记录
n2.target,并调用mountChildren挂载子节点
更新阶段需要先处理子节点本身的变化,再处理目标容器的变化:
- 调用
patchChildren,在旧容器中更新子节点 - 对比新旧
to和disabled - 如果目标容器发生变化,就使用
insert将子节点移动到新的容器 - 更新
n2.target,保存当前实际使用的容器
4. 卸载 Teleport
Teleport 没有对应的真实 DOM 元素,真正插入页面的是它的子节点。
function unmount(vnode) {
const { shapeFlag, children, ref } = vnode
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component)
}
else if (shapeFlag & ShapeFlags.TELEPORT) {
unmountChildren(children)
return
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(children)
}
hostRemove(vnode.el)
if (ref != null) {
setRef(ref, null)
}
}
简单处理一下,只卸载子节点,然后直接 return。
如果继续执行 hostRemove(vnode.el),就会尝试删除 Teleport 自身的 DOM 节点。但 Teleport 没有对应的真实元素,实际需要删除的是已经挂载到目标容器中的子节点。