Vue3.0 新增了一个Teleport
组件,开发者可以使用它将其所在组件模板的部分内容移动到特定的DOM位置,譬如body
或者其他任意位置。
Vue 2.0要实现对应的功能则需要使用portal-vue
三方库,或者使用$el
操作DOM等来实现。
接下来我们就从使用方式和实现原理两个方面来分别介绍。
Teleport
组件的使用
Teleport
组件的使用很简单,把需要移动的内容包起来即可:
<teleport :to="body" :disabled="false">
<div>需要移动的内容</div>
</teleport>
上面这些代码的表现结果是
<div>需要移动的内容</div>
会渲染在body上,而不是所在的组件的模板所在的位置。
Teleport
有两个参数:
to
为需要移动的位置,可以是选择器也可以是DOM节点;disabled
如果为true
,内容不进行移动,disabled
如果为false
, 则Teleport
包裹的元素节点会被移动到to
的节点下。
例子:实现某部分内容在 组件的模板内, 子组件的模板内 和 body 间切换。
- 子组件有一个
#teleport1
节点
<!--SubContainer.vue-->
<template>
<div id="teleport1">
<h4>子组件</h4>
</div>
</template>
- APP组件包含子组件,有一个按钮
button
切换位置 和 需要传送的内容<div class="send_content">{{ showingString }}</div>
<template>
<sub-container />
<button class="btn" @click="changePosition">传送门</button>
<teleport :to="to" :disabled="disabled">
<div class="send_content">{{ showingString }}</div>
</teleport>
</template>
<script lang="ts">
import SubContainer from "./components/SubContainer.vue";
import { defineComponent, ref } from "vue";
enum TeleportPosition {
currentInstance, // 当前组件
subInstance, // 子组件
body, // body
}
export default defineComponent({
name: "App",
components: {
SubContainer,
},
setup() {
// 位置
let position = ref(TeleportPosition.currentInstance);
// 显示的字符串内容
let showingString = ref("内容显示在APP组件内");
// 是否禁用teleport
let disabled = ref(true);
// 挂载的DOM节点
let to = ref("body");
// 切换位置
let changePosition = () => {
if (position.value == TeleportPosition.currentInstance) {
position.value = TeleportPosition.subInstance;
showingString.value = "内容显示在子组件内";
disabled.value = false;
to.value = "#teleport1";
} else if (position.value == TeleportPosition.subInstance) {
position.value = TeleportPosition.body;
showingString.value = "内容显示在body内";
disabled.value = false;
to.value = "body";
} else {
position.value = TeleportPosition.currentInstance;
showingString.value = "内容显示在APP组件内";
disabled.value = true;
to.value = "body";
}
};
return { showingString, to, disabled, changePosition };
},
});
</script>
- 上面这些代码就实现了
<div class="send_content">{{ showingString }}</div>
这部分DOM内容可以在 APP组件的DOM节点,子组件的DOM节点 和 body 上选择挂载。
Teleport
组件的实现原理
Teleport
组件的挂载
我们知道组件的挂载首先会进入patch
函数:
<!-- render.ts -->
const patch: PatchFn = (
) => {
// 省略其他...
// 处理TELEPORT组件
if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
}
patch
函数执行时如果发现VNode是Teleport
组件,则执行对应TeleportImpl
的process
方法。
// 1. 在主视图插入注释节点或者空白文本节点
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)
// 2. 获取目标元素节点
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = (n2.targetAnchor = createText(''))
if (target) {
insert(targetAnchor, target)
isSVG = isSVG || isTargetSVG(target)
}
const mount = (container: RendererElement, anchor: RendererNode) => {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
children as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
// 3. 在目标元素插入`Teleport`组件的子节点
if (disabled) {
mount(container, mainAnchor)
} else if (target) {
mount(target, targetAnchor)
}
具体逻辑如下:
- 创建一个节点
mainAnchor
, 开发环境下是一个注释节点,在发布环境是一个空文本节点, 将这个创建的mainAnchor
节点挂载在父组件对应的DOM节点下;
2.使用querySelector
找到Teleport
组件to
属性指定的节点target
目标节点,然后在targetAnchor
节点下创建一个空文本节点做为锚定节点;
3.如果Teleport
组件disabled
属性值为true
,将Teleport
组件的子节点挂载在mainAnchor
h,如果disabled
属性值为false
,将Teleport
组件的子节点挂载在目标节点targetAnchor
。
Teleport
组件的更新
// 数据
n2.el = n1.el
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
const wasDisabled = isTeleportDisabled(n1.props)
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
isSVG = isSVG || isTargetSVG(target)
// 1. 更新子节点
if (dynamicChildren) {
// fast path when the teleport happens to be a block root
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
currentContainer,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
)
traverseStaticChildren(n1, n2, true)
} else if (!optimized) {
patchChildren(
n1,
n2,
currentContainer,
currentAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
false
)
}
// 根据disabled 和 to 进行分别操作
if (disabled) {
if (!wasDisabled) {
moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE)
}
} else {
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 (wasDisabled) {
moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
}
}
具体流程如下:
- 更新子节点,分为全量更新和优化更新;
- 如果新节点
disabled
为true
,而旧节点disabled
是false
,把新节点移回到主视图节点mainAnchor
;- 如果新节点
disabled
为false
,to
节点有变化,则把新节点移动到to
节点;- 如果新节点
disabled
为false
,to
节点没有变化,如果旧节点disabled
是true
, 新节点从到主视图节点移动到目标节点targetAnchor
;
至此,更新节点完成。
Teleport
组件的移除
我们知道组件的卸载首先会进入unmount
方法:
if (shapeFlag & ShapeFlags.TELEPORT) {
;(vnode.type as typeof TeleportImpl).remove(
vnode,
parentComponent,
parentSuspense,
optimized,
internals,
doRemove
)
}
如果是Teleport
组件,则直接调用TeleportImpl
的remove
方法;
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
// 1.
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
)
}
}
}
}
具体流程如下:
- 如果有目标元素,则先移除目标元素;
- 移除主视图的元素;
- 移除子节点元素;
至此,移除节点完成。
一个思考题
<template>
<button class="btn" @click="changePosition">传送门</button>
<teleport :to="to" :disabled="disabled">
<div class="send_content">{{ showingString }}</div>
</teleport>
<sub-container />
</template>
如果我们的案例中,子组件在Teleport
组件的后面,此时Teleport
组件是否能正常的显示?