文章目录
- 前言
- 一、基本用法
-
- [1.1 为什么需要 Teleport](#1.1 为什么需要 Teleport)
- [1.2 Teleport 解决](#1.2 Teleport 解决)
- [二、to 属性](#二、to 属性)
-
- [2.1 指定目标位置](#2.1 指定目标位置)
- [2.2 挂载点准备](#2.2 挂载点准备)
- [2.3 disabled 动态控制](#2.3 disabled 动态控制)
- 三、典型场景
-
- [3.1 Modal 对话框](#3.1 Modal 对话框)
- [3.2 Toast 全局通知](#3.2 Toast 全局通知)
- [3.3 全屏 Loading](#3.3 全屏 Loading)
- [3.4 Dropdown 下拉菜单](#3.4 Dropdown 下拉菜单)
- 四、逻辑归属与事件冒泡
-
- [4.1 组件树 vs DOM 树](#4.1 组件树 vs DOM 树)
- [4.2 事件冒泡遵循组件树](#4.2 事件冒泡遵循组件树)
- [4.3 多个 Teleport 到同一目标](#4.3 多个 Teleport 到同一目标)
- [五、SSR 行为](#五、SSR 行为)
-
- [5.1 默认原位渲染](#5.1 默认原位渲染)
- [5.2 处理方案](#5.2 处理方案)
- 六、面试聚焦
-
- [6.1 事件冒泡遵循组件树](#6.1 事件冒泡遵循组件树)
- [6.2 Teleport 在 SSR 时的行为](#6.2 Teleport 在 SSR 时的行为)
- [6.3 Teleport 解决了什么问题?](#6.3 Teleport 解决了什么问题?)
- 七、易混淆点
- 八、思考与练习
- 总结
前言
<Teleport> 是 Vue 3 内置组件,可将子元素的 DOM 传送到页面其他位置,突破父容器的 CSS 限制。本篇会讲清楚:
- Teleport 的基本用法与
to属性 - Modal、Toast、Loading 等典型场景
- 逻辑归属与事件冒泡规则
- SSR 下的行为
一、基本用法
1.1 为什么需要 Teleport
vue
<!-- 问题:Modal 嵌套在组件内,可能被父容器裁切 -->
<div class="page" style="overflow: hidden; position: relative;">
<button @click="open = true">打开弹窗</button>
<div v-if="open" class="modal"> <!-- z-index 可能失效 -->
弹窗内容
</div>
</div>
父容器的 overflow: hidden、transform、z-index 等会限制子元素显示。Modal 需要渲染到 <body> 下才能全屏覆盖。
1.2 Teleport 解决
vue
<template>
<button @click="open = true">打开弹窗</button>
<Teleport to="body">
<div v-if="open" class="modal-mask">
<div class="modal-content">
<p>弹窗内容</p>
<button @click="open = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
<style>
.modal-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
</style>
DOM 渲染到 <body> 末尾,但组件的 props、emit、生命周期仍属于父组件。
二、to 属性
2.1 指定目标位置
vue
<!-- 传送到 body -->
<Teleport to="body">...</Teleport>
<!-- 传送到指定 id -->
<Teleport to="#modal-root">...</Teleport>
<!-- 传送到 class -->
<Teleport to=".app-container">...</Teleport>
to 接受 CSS 选择器字符串,目标元素必须已存在于 DOM 中。
2.2 挂载点准备
html
<!-- index.html -->
<body>
<div id="app"></div>
<div id="modal-root"></div> <!-- 可选:专用挂载点 -->
</body>
vue
<Teleport to="#modal-root">
<Modal v-if="visible" />
</Teleport>
2.3 disabled 动态控制
vue
<Teleport to="body" :disabled="isMobile">
<Modal />
</Teleport>
disabled 为 true 时,内容渲染在 Teleport 原位(不传送),SSR 或移动端回退时常用。
三、典型场景
3.1 Modal 对话框
vue
<!-- components/Modal.vue -->
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="modelValue" class="modal-mask" @click.self="close">
<div class="modal-box">
<slot />
<button @click="close">关闭</button>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue'])
const close = () => emit('update:modelValue', false)
</script>
配合 Transition 实现淡入淡出,Teleport 确保遮罩覆盖全屏。
3.2 Toast 全局通知
vue
<!-- components/Toast.vue -->
<template>
<Teleport to="body">
<div class="toast-container">
<TransitionGroup name="toast">
<div v-for="msg in messages" :key="msg.id" class="toast-item">
{{ msg.text }}
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<style>
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
}
</style>
Toast 固定在视口右上角,不受页面滚动和父容器影响。
3.3 全屏 Loading
vue
<template>
<Teleport to="body">
<div v-if="loading" class="loading-mask">
<div class="spinner">加载中...</div>
</div>
</Teleport>
</template>
<style>
.loading-mask {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
</style>
3.4 Dropdown 下拉菜单
vue
<template>
<div class="dropdown-trigger" @click="open = !open">
选择项
</div>
<Teleport to="body">
<div
v-if="open"
class="dropdown-menu"
:style="{ top: menuTop + 'px', left: menuLeft + 'px' }"
>
<div v-for="item in options" :key="item">{{ item }}</div>
</div>
</Teleport>
</template>
下拉菜单传送到 body,避免被 overflow: auto 的表格或侧边栏裁切。
四、逻辑归属与事件冒泡
4.1 组件树 vs DOM 树
Vue 组件树:App → Page → Modal(逻辑父子)
DOM 树: body → div#app → ... ; body → div.modal(Teleport 渲染位置)
Teleport 只改变 DOM 位置,不改变 Vue 组件树:
- props / emit 仍走父组件
- 生命周期仍属于定义 Teleport 的组件
- 父组件可通过
v-if控制 Teleport 内容
4.2 事件冒泡遵循组件树
vue
<!-- Parent.vue -->
<template>
<div @click="handleParentClick">
<Child />
</div>
</template>
<!-- Child.vue -->
<template>
<Teleport to="body">
<button @click="handleClick">按钮</button>
</Teleport>
</template>
点击 Teleport 内的按钮:
@click在 Child 组件内触发 → 沿 Vue 组件树 向 Parent 冒泡- 不会冒泡到 DOM 中 Teleport 的实际父元素(如 body)
4.3 多个 Teleport 到同一目标
vue
<Teleport to="#modal-root">
<ModalA v-if="showA" />
</Teleport>
<Teleport to="#modal-root">
<ModalB v-if="showB" />
</Teleport>
多个 Teleport 传送到同一容器时,按声明顺序追加到目标内。
五、SSR 行为
5.1 默认原位渲染
服务端渲染时,Teleport 默认不传送,内容渲染在组件原位 DOM 中。
客户端 hydration 完成后才传送到 to 目标,可能造成短暂闪烁。
5.2 处理方案
vue
<!-- 方案一:SSR 时禁用 Teleport -->
<Teleport to="body" :disabled="!isClient">
<Modal />
</Teleport>
<script setup>
import { ref, onMounted } from 'vue'
const isClient = ref(false)
onMounted(() => { isClient.value = true })
</script>
javascript
// 方案二:Nuxt 等框架内置 SSR Teleport 支持
// 需在服务端提供 #teleports 挂载点
生产环境使用 Nuxt 等 SSR 框架时,按框架文档配置 Teleport 挂载点。
六、面试聚焦
6.1 事件冒泡遵循组件树
Teleport 只移动 DOM,不改变 Vue 组件树。事件仍按组件父子关系冒泡,不会冒泡到 DOM 中的实际父元素。
6.2 Teleport 在 SSR 时的行为
默认在原位渲染,不传送。客户端激活后才移动到目标位置。可通过 disabled 或框架配置处理。
6.3 Teleport 解决了什么问题?
Modal、Toast 等需要全屏/固定定位的 UI,避免被父容器 overflow、z-index、transform 裁切或遮挡。
七、易混淆点
- 只改 DOM 不改组件树:props/emit/生命周期仍属原组件。
- 事件不沿 DOM 冒泡:沿 Vue 组件树冒泡。
- to 目标必须存在:挂载前 DOM 中需有对应元素。
- SSR 默认不传送:hydration 后才移动,注意闪烁问题。
- 与 Portal 类似:React Portal 概念相同,Vue 3 内置为 Teleport。
八、思考与练习
1. Teleport 的作用是什么?
解析:将子元素 DOM 传送到页面其他位置(如 body),突破父容器 CSS 限制,逻辑仍属原组件。
2. Modal 为什么常用 Teleport 到 body?
解析:避免被父容器 overflow、z-index、transform 裁切,确保遮罩全屏覆盖。
3. Teleport 内点击事件如何冒泡?
解析:遵循 Vue 组件树冒泡,不遵循 DOM 树。事件向定义 Teleport 的组件的父级传递。
4. SSR 时 Teleport 的行为?
解析:默认在原位渲染,客户端 hydration 后才传送到 to 目标。
5. disabled 属性有什么用?
解析:为 true 时不传送,内容渲染在原位。SSR 回退或条件禁用传送时使用。
总结
- Teleport:Vue 3 内置,将 DOM 传送到指定位置,逻辑仍属原组件
- to:CSS 选择器指定目标(body、#id、.class)
- 典型场景:Modal、Toast、Loading、Dropdown
- 事件冒泡:遵循组件树,不遵循 DOM 树
- SSR:默认原位渲染,需注意 hydration 闪烁