- 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
// 是否禁用 Teleport
const disabled = isTeleportDisabled(n2.props)
let { shapeFlag, children, dynamicChildren } = n2
// #3302
// HMR updated, force full diff
if (__DEV__ && isHmrUpdating) {
optimized = false
dynamicChildren = 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 tree
isSVG = 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
// 是否禁用 Teleport
const disabled = isTeleportDisabled(n2.props)
let { shapeFlag, children, dynamicChildren } = n2
// #3302
// HMR updated, force full diff
if (__DEV__ && isHmrUpdating) {
optimized = false
dynamicChildren = null
}
if (n1 == null) {
// ---------------------------
// 创建逻辑
// ---------------------------
} else {
// update content
n2.el = n1.el
const 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 : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
isSVG = isSVG || isTargetSVG(target)
// 更新子节点
if (dynamicChildren) {
// fast path when the teleport happens to be a block root
patchBlockChildren(
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 } = vnode
if (target) {
hostRemove(targetAnchor!)
}
// an unmounted teleport should always remove its children if not disabled
if (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
)
}
}
}
}