青橙记录追剧
110.94M · 2026-02-27
小弟年后开工就遇到了大问题,有一个组件原先使用了直接挂载的方式,但是数据传递极其离谱,需要从子组件b传递到父组件a,再传递到子组件c,a和c是兄弟节点,但是数据需要传递,很繁琐,于是我一拍脑袋,把组件c改成了组件b的子组件,父子组件传参方便多了,再配合vue3提供的Teleport传递到想要挂载的适合的位置,配合父组件a的样式,这样子才不乱套。
自测的时候本地环境还好好的,一打包到具体项目就歇菜。明明也没有报错,数据传递的也没有问题,但是就是Teleport传递的位置看不到任何的元素在里边,就像年后的脑袋空空如也
在使用 Vue3 的 Teleport 组件时,我们遇到了一个有趣的问题:
在开发环境中,以下代码工作正常:
<!-- GridView.vue -->
<Teleport to="#header-canvas-container">
<div class="header-content">
<!-- 表头内容 -->
</div>
</Teleport>
<!-- App.vue -->
<div class="operation-container">
<div v-if="isDocx" id="header-canvas-container"/>
</div>
但在打包后集成到其他项目时
#header-canvas-container 容器存在但是空的,Teleport 的内容没有被传送过去,且控制台没有任何报错。
document.querySelector 能找到)defer 属性也无法解决Vue3 的组件渲染遵循以下流程:
┌─────────────────────────────────────────────┐
│ 组件挂载/更新流程 │
├─────────────────────────────────────────────┤
│ 1. Setup 状态初始化 │
│ 2. Render 生成 VNode │
│ 3. Patch 将 VNode 转换为真实 DOM │
│ 4. PostFlush 副作用(watch、onMounted等) │
└─────────────────────────────────────────────┘
Teleport 是 Vue3 提供的特殊内置组件,用于将内容渲染到 DOM 的其他位置:
<Teleport to="body">
<div>这段内容会被移动到 body 下</div>
</Teleport>
实现原理(简化版):
// Vue 内部伪代码
const Teleport = {
process(n1, n2, container) {
const target = document.querySelector(n2.props.to)
if (n2.props.defer) {
// defer 模式:延迟到父组件更新完成后
queuePostRenderEffect(() => {
moveTeleportContent(n2, target)
})
} else {
// 立即模式:在当前渲染周期执行
moveTeleportContent(n2, target)
}
}
}
defer 属性的真实含义defer 是 Vue 3.2+ 引入的属性,它的作用是:
文档描述:
实际执行时机:
// defer 的实现逻辑
if (defer) {
// 等待当前组件的 patch 完成后
// 但不等待父组件或浏览器的布局计算
queuePostFlushCb(() => {
doTeleport()
})
}
关键点:
理解这个问题需要了解浏览器的渲染流程:
┌──────────────────────────────────────────────┐
│ 单个事件循环(Event Loop) │
├──────────────────────────────────────────────┤
│ 1. 执行宏任务(Script、setTimeout等) │
│ 2. 执行微任务队列(Promise.then、Mutation等) │
│ 3. 渲染管线(如果需要渲染): │
│ a. 样式计算(Style) │
│ b. 布局(Layout) │
│ c. 绘制(Paint) │
│ d. 合成(Composite) │
└──────────────────────────────────────────────┘
nextTick vs setTimeout// nextTick: 在当前宏任务结束前执行
await nextTick()
// 相当于:Promise.resolve().then(fn)
// setTimeout: 在下一个宏任务执行
setTimeout(() => {}, 0)
// 相当于:添加到宏任务队列末尾
开发环境:
时间线:
0ms - 开始渲染 GridView
10ms - GridView 渲染完成,defer 执行
15ms - App.vue 布局稳定
20ms - 浏览器完成布局计算
即使 defer 执行较早,但开发环境的渲染较慢,浏览器有时间"追赶"上来。
生产环境:
时间线:
0ms - 开始渲染 GridView
2ms - GridView 渲染完成,defer 执行
3ms - App.vue 还在计算布局
5ms - 浏览器完成布局计算
打包优化后渲染速度极快,defer 可能在布局稳定前就执行了。
布局依赖链:
App.vue 渲染
→ operation-container 计算 layout
→ header-canvas-container 确定 final position
→ Teleport can successfully attach
当 operation-container 高度为 0 时:
defer 执行时,这个计算可能还没完成| 方案 | 可靠性 | 性能 | 复杂度 |
|---|---|---|---|
defer 属性 | ️ 生产环境不稳定 | 最优 | 简单 |
setTimeout | 稳定 | ️ 略低(可忽略) | 简单 |
watch + nextTick | 稳定 | 良好 | ️ 中等 |
| 手动 DOM 操作 | 最稳定 | 较低 | 复杂 |
// GridView.vue
const shouldTeleport = ref(false)
watch(() => toValue(isDocx), async (newIsDocx) => {
if (!newIsDocx) {
shouldTeleport.value = false
return
}
// 1. 等待 Vue 的 DOM 更新队列
await nextTick()
// 2. 验证目标容器存在
const target = document.querySelector('#header-canvas-container')
if (!target) {
console.warn('Teleport target not found')
return
}
// 3. 等待浏览器的渲染管线完成
setTimeout(() => {
shouldTeleport.value = true
}, 0)
}, { immediate: true })
<template>
<Teleport v-if="shouldTeleport && isDocx" to="#header-canvas-container">
<!-- 内容 -->
</Teleport>
</template>
时刻 | Vue 状态 | 浏览器状态 | 操作
-----|-----------------|------------------|------------------
T1 | GridView开始渲染 | | watch触发
T2 | 执行nextTick | | 等待Vue队列
T3 | Vue队列完成 | 开始布局计算 | querySelector
T4 | | 布局计算中... | setTimeout推入队列
T5 | | 布局完成 |
T6 | | 渲染完成 |
T7 | shouldTeleport=true| | Teleport执行
defer适合场景:
body 或其他全局容器不适合场景:
setTimeout适合场景:
不适合场景:
Vue3 使用了渲染批处理(Render Batching):
// Vue 会自动批处理多次状态更新
count.value++ // 不会立即渲染
count.value++ // 不会立即渲染
name.value = 'new' // 不会立即渲染
// ← 这里才统一渲染
但这只适用于同一个事件循环内的更新。setTimeout 会创建新的事件循环,打破批处理。
Vue3 的 Teleport 在源码中(runtime-core/src/components/Teleport.ts):
// 简化的源码逻辑
process(
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null
) {
const targetSelector = n2.props.to
const target = document.querySelector(targetSelector)
if (n2.props.defer) {
// 使用 queuePostRenderEffect 延迟
queuePostRenderEffect(() => {
move(n2, target)
})
} else {
// 立即执行
move(n2, target)
}
}
关键点:defer 只使用了 queuePostRenderEffect,这不会等待浏览器的渲染管线。
// 会触发 Reflow 的操作
element.offsetHeight // 读取布局信息
element.style.height = '100px' // 修改样式
element.appendChild(child) // 修改 DOM 树
// Reflow 是昂贵的操作,浏览器会批量处理
setTimeout 确保在这些 Reflow 完成后才执行 Teleport。
Teleport 的 defer 属性只等待 Vue 的渲染完成,不等待浏览器的布局计算。在打包后渲染速度变快的情况下,可能在目标容器的位置确定前就执行传送。
使用 nextTick() + setTimeout() 组合:
nextTick() 等待 Vue 的 DOM 更新setTimeout() 等待浏览器的渲染管线| 概念 | 说明 |
|---|---|
| Vue 渲染队列 | Vue 自己的更新批处理机制 |
| 浏览器渲染管线 | Style → Layout → Paint → Composite |
nextTick() | 在 Vue 队列完成后执行 |
setTimeout(,0) | 在下一个宏任务执行(等待浏览器渲染完成) |
defer | 只等待 Vue,不等待浏览器 |
这类问题不仅存在于 Teleport,也适用于:
记住:Vue 的"DOM 更新完成" ≠ 浏览器的"渲染完成"。