Vue3.0 新增了一个Teleport
组件,开发者可以使用它将其所在组件模板的部分内容移动到特定的DOM位置,譬如body
或者其他任意位置。
Vue 2.0要实现对应的功能则需要使用portal-vue
三方库,或者使用$el
操作DOM等来实现。
接下来我们就从使用方式和实现原理两个方面来分别介绍。
Teleport
组件的使用
Teleport
组件的使用很简单,把需要移动的内容包起来即可:
需要移动的内容
上面这些代码的表现结果是
会渲染在body上,而不是所在的组件的模板所在的位置。
需要移动的内容
Teleport
有两个参数:
to
为需要移动的位置,可以是选择器也可以是DOM节点;disabled
如果为true
,内容不进行移动,disabled
如果为false
, 则Teleport
包裹的元素节点会被移动到to
的节点下。
例子:实现某部分内容在 组件的模板内, 子组件的模板内 和 body 间切换。
- 子组件有一个
#teleport1
节点
子组件
- APP组件包含子组件,有一个按钮
button
切换位置 和 需要传送的内容{{ showingString }}
{{ showingString }}
- 上面这些代码就实现了
这部分DOM内容可以在 APP组件的DOM节点,子组件的DOM节点 和 body 上选择挂载。{{ showingString }}
Teleport
组件的实现原理
Teleport
组件的挂载
我们知道组件的挂载首先会进入patch
函数:
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节点下;- 使用
querySelector
找到Teleport
组件to
属性指定的节点target
目标节点,然后在targetAnchor
节点下创建一个空文本节点做为锚定节点;- 如果
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
)
}
}
}
}
具体流程如下:
- 如果有目标元素,则先移除目标元素;
- 移除主视图的元素;
- 移除子节点元素;
至此,移除节点完成。
一个思考题
{{ showingString }}
如果我们的案例中,子组件在Teleport
组件的后面,此时Teleport
组件是否能正常的显示?