- Contents
相关代码位置如下:
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/renderer.ts
Teleport 组件使用起来非常简单,套在想要在别处渲染的组件或者 DOM 节点的外部,然后通过 to 这个 prop 去指定渲染到的位置,to 可以是一个 DOM 选择器字符串,也可以是一个 DOM 节点。
// Dialog.vue<script setup>import { ref } from 'vue'const visible = ref(false)const show = () => {visible.value = true}</script><template><div v-show="visible" class="dialog"><div class="dialog-body"><p>I'm a dialog!</p><button @click="visible = false">Close</button></div></div></template><style lang="css" scoped>.dialog {position: absolute;top: 0;right: 0;bottom: 0;left: 0;background-color: rgba(0, 0, 0, .5);display: flex;flex-direction: column;align-items: center;justify-content: center;}.dialog .dialog-body {display: flex;flex-direction: column;align-items: center;justify-content: center;background-color: white;width: 300px;height: 300px;padding: 5px;}</style>
<script setup>import { ref } from 'vue'import Dialog from './components/Dialog.vue'const dialog = ref(null)const showDialog = () => {dialog.value?.show()}</script><template><button @click="showDialog">Show dialog</button><Dialog ref="dialog"></Dialog></template>
因为 Dialog 组件使用的是 position:absolute 绝对定位的方式,如果它的父级 DOM 有 position 不为 static 的布局方式,那么 Dialog 的定位就受到了影响,不能按预期渲染了。
所以一种好的解决方案是把 Dialog 组件渲染的这部分 DOM 挂载到 body 下面,这样就不会受到父级样式的影响了。
这也是 Teleport 组件要解决的问题。将指定内容渲染到特定容器中(跨 DOM 层级的渲染),而不受 DOM 层级的限制。
<script setup>import { ref } from 'vue'import Dialog from './components/Dialog.vue'const dialog = ref(null)const showDialog = () => {dialog.value?.show()}</script><template><button @click="showDialog">Show dialog</button><!-- 新增 Teleport --><teleport to="body"><Dialog ref="dialog"></Dialog></teleport></template>
Teleport 实现原理
<teleport to="body"><Dialog ref="dialog"></Dialog></teleport>
模版编译之后的结果:Teleport Explorer
import { resolveComponent as _resolveComponent, createVNode as _createVNode, Teleport as _Teleport, openBlock as _openBlock, createBlock as _createBlock } from "vue"export function render(_ctx, _cache, $props, $setup, $data, $options) {const _component_Dialog = _resolveComponent("Dialog")return (_openBlock(), _createBlock(_Teleport, { to: "body" }, [_createVNode(_component_Dialog, { ref: "dialog" }, null, 512 /* NEED_PATCH */)]))}
源码实现如下
export const TeleportImpl = {__isTeleport: true,process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals) {if (n1 == null) {// 创建逻辑} else {// 更新逻辑}},remove(vnode,parentComponent,parentSuspense,optimized,{ um: unmount, o: { remove: hostRemove } }: RendererInternals,doRemove: Boolean) {// 删除逻辑},move: moveTeleport,hydrate: hydrateTeleport,}
Teleport 组件的实现就是一个对象,对外提供了几个方法。其中 process 方法负责组件的创建和更新逻辑,remove 方法负责组件的删除逻辑。
创建
Teleport 组件创建部分主要分为三个步骤:
- 在主视图里插入注释节点或者空白文本节点

- 获取目标元素节点
- 往目标元素插入 Teleport 组件的子节点
const process = (n1: TeleportVNode | null,n2: TeleportVNode,container: RendererElement,anchor: RendererNode | null,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,isSVG: boolean,slotScopeIds: string[] | null,optimized: boolean,internals: RendererInternals) => {const {mc: mountChildren,pc: patchChildren,pbc: patchBlockChildren,o: { insert, querySelector, createText, createComment }} = internals// 是否禁用 Teleportconst disabled = isTeleportDisabled(n2.props)let { shapeFlag, children, dynamicChildren } = n2// #3302// HMR updated, force full diffif (__DEV__ && isHmrUpdating) {optimized = falsedynamicChildren = null}if (n1 == null) {// 在主视图里插入注释节点或者空白文本节点const placeholder = (n2.el = __DEV__? createComment('teleport start'): createText(''))const mainAnchor = (n2.anchor = __DEV__? createComment('teleport end'): createText(''))insert(placeholder, container, anchor)insert(mainAnchor, container, anchor)// 获取目标元素的 DOM 节点const target = (n2.target = resolveTarget(n2.props, querySelector))const targetAnchor = (n2.targetAnchor = createText(''))if (target) {insert(targetAnchor, target)// #2652 we could be teleporting from a non-SVG tree into an SVG treeisSVG = isSVG || isTargetSVG(target)} else if (__DEV__ && !disabled) {warn('Invalid Teleport target on mount:', target, `(${typeof target})`)}const mount = (container: RendererElement, anchor: RendererNode) => {// Teleport *always* has Array children. This is enforced in both the// compiler and vnode children normalization.if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 挂载子节点// (遍历 Teleport 组件的 children 属性,并逐一调用 patch 函数完成子节点的挂载)mountChildren(children as VNodeArrayChildren,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)}}if (disabled) {// disabled 则在原来的位置挂载mount(container, mainAnchor)} else if (target) {// 挂载在 target 元素的位置mount(target, targetAnchor)}}}
更新
当组件发生更新的时候,仍然会执行 patch 逻辑走到 Teleport 的 process 方法,去处理 Teleport 组件的更新
const process = (n1: TeleportVNode | null,n2: TeleportVNode,container: RendererElement,anchor: RendererNode | null,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,isSVG: boolean,slotScopeIds: string[] | null,optimized: boolean,internals: RendererInternals) => {const {mc: mountChildren,pc: patchChildren,pbc: patchBlockChildren,o: { insert, querySelector, createText, createComment },} = internals// 是否禁用 Teleportconst disabled = isTeleportDisabled(n2.props)let { shapeFlag, children, dynamicChildren } = n2// #3302// HMR updated, force full diffif (__DEV__ && isHmrUpdating) {optimized = falsedynamicChildren = null}if (n1 == null) {// ---------------------------// 创建逻辑// ---------------------------} else {// update contentn2.el = n1.elconst mainAnchor = (n2.anchor = n1.anchor)!const target = (n2.target = n1.target)!const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!// 之前是不是 disabled 状态const wasDisabled = isTeleportDisabled(n1.props)const currentContainer = wasDisabled ? container : targetconst currentAnchor = wasDisabled ? mainAnchor : targetAnchorisSVG = isSVG || isTargetSVG(target)// 更新子节点if (dynamicChildren) {// fast path when the teleport happens to be a block rootpatchBlockChildren(n1.dynamicChildren!,dynamicChildren,currentContainer,parentComponent,parentSuspense,isSVG,slotScopeIds)// even in block tree mode we need to make sure all root-level nodes// in the teleport inherit previous DOM references so that they can// be moved in future patches.traverseStaticChildren(n1, n2, true)} else if (!optimized) {patchChildren(n1,n2,currentContainer,currentAnchor,parentComponent,parentSuspense,isSVG,slotScopeIds,false)}if (disabled) {if (!wasDisabled) {// enabled -> disabled// 把子节点从目标元素移动回主容器内moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE)}} else {// 目标容器改变// (to 属性值修改)// 新旧 to 属性值不同,则需要对内容进行移动if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {// 获取新的目标元素const nextTarget = (n2.target = resolveTarget(n2.props, querySelector))if (nextTarget) {// 移动到新的目标元素moveTeleport(n2, nextTarget, null, internals, TeleportMoveTypes.TARGET_CHANGE)} else if (__DEV__) {warn('Invalid Teleport target on update:', target, `(${typeof target})`)}} else if (wasDisabled) {// disabled -> enabled// 移动到目标元素位置moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)}}}}
Teleport 组件更新:
- 更新子节点
- 更新分为优化更新和普通的全量比对更新两种情况
- 处理 disabled 属性变化的情况
- 新节点
disabled为true,且旧节点的disabled为false,则需要把 Teleport 的子节点从目标元素内部移回到主视图内 - 新节点
disabled为false- 先通过
to属性是否改变来判断目标元素target有没有变化- 有变化,则把 Teleport 的子节点移动到新的
target内部 - 没变化
- 则判断旧节点的
disabled是否为true- 是,则把 Teleport 的子节点从主视图内部移动到目标元素内部
- 则判断旧节点的
- 有变化,则把 Teleport 的子节点移动到新的
- 先通过
- 新节点
移除
当组件移除的时候会执行 unmount 方法,它的内部会判断如果移除的组件是一个 Teleport 组件,就会执行组件的 remove 方法
if (shapeFlag & 64 /* TELEPORT */) {vnode.type.remove(vnode, internals);}if (doRemove) {remove(vnode);}
Teleport 的 remove 首先通过 hostRemove 移除主视图渲染的 target 的 teleport start 注释节点以及 Teleport 主视图的元素 teleport end 注释节点,然后再去遍历 Teleport 的子节点执行 remove 移除,至此,Teleport 组件完成了移除。
const remove = (vnode: VNode,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,optimized: boolean,{ um: unmount, o: { remove: hostRemove } }: RendererInternals,doRemove: Boolean) => {const { shapeFlag, children, anchor, targetAnchor, target, props } = vnodeif (target) {hostRemove(targetAnchor!)}// an unmounted teleport should always remove its children if not disabledif (doRemove || !isTeleportDisabled(props)) {hostRemove(anchor!)if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {for (let i = 0; i < (children as VNode[]).length; i++) {const child = (children as VNode[])[i]unmount(child,parentComponent,parentSuspense,true,!!child.dynamicChildren)}}}}
