Teleport 传送门

文章目录

  • 前言
  • 一、基本用法
    • [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: hiddentransformz-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>

disabledtrue 时,内容渲染在 Teleport 原位(不传送),SSR 或移动端回退时常用。


三、典型场景

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>
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,避免被父容器 overflowz-indextransform 裁切或遮挡。


七、易混淆点

  1. 只改 DOM 不改组件树:props/emit/生命周期仍属原组件。
  2. 事件不沿 DOM 冒泡:沿 Vue 组件树冒泡。
  3. to 目标必须存在:挂载前 DOM 中需有对应元素。
  4. SSR 默认不传送:hydration 后才移动,注意闪烁问题。
  5. 与 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 闪烁