Teleport 组件解决的问题
版本:3.2.31
如果要实现一个 “蒙层” 的功能,并且该 “蒙层” 可以遮挡页面上的所有元素,通常情况下我们会选择直接在 标签下渲染 “蒙层” 内容。如果在Vue.js 2 中实现这个功能,只能通过原生 DOM API 来手动搬运 DOM元素实现,这就会使得元素的渲染与 Vue.js 的渲染机制脱节,并会导致各种可预见或不可遇见的问题。
Vue.js 3 中内建的 Teleport 组件,可以将指定内容渲染到特定容器中,而不受DOM层级的限制。可以很好的解决这个问题。
下面,我们来看看 Teleport 组件是如何解决这个问题的。如下是基于 Teleport 组件实现的蒙层组件的模板:
<template>
<Teleport to="body">
<div class="overlay"></div>
</Teleport>
</template>
<style scoped>
.verlay {
z-index: 9999;
}
</style>
可以看到,蒙层组件要渲染的内容都包含在 Teleport 组件内,即作为 Teleport 组件的插槽。
通过为 Teleport 组件指定渲染目标 body,即 to 属性的值,该组件就会把它的插槽内容渲染到 body 下,而不会按照模板的 DOM 层级来渲染,于是就实现了跨 DOM 层级的渲染。
从而实现了蒙层可以遮挡页面中的所有内容。
Teleport 组件的基本结构
// packages/runtime-core/src/components/Teleport.ts
export const TeleportImpl = {
// Teleport 组件独有的特性,用作标识
__isTeleport: true,
// 客户端渲染 Teleport 组件
process() {},
// 移除 Teleport
remove() {},
// 移动 Teleport
move: moveTeleport,
// 服务端渲染 Teleport
hydrate: hydrateTeleport
}
export const Teleport = TeleportImpl as any as {
__isTeleport: true
new (): { $props: VNodeProps & TeleportProps }
}
我们对 Teleport 组件的源码做了精简,如上面的代码所示,可以看到,一个组件就是一个选项对象。Teleport 组件上有 __isTeleport、process、remove、move、hydrate 等属性。其中 __isTeleport 属性是 Teleport 组件独有的特性,用作标识。process 函数是渲染 Teleport 组件的主要渲染逻辑,它从渲染器中分离出来,可以避免渲染器逻辑代码 “膨胀”。
Teleport 组件 process 函数
process 函数主要用于在客户端渲染 Teleport 组件。由于 Teleport 组件需要渲染器的底层支持,因此将 Teleport 组件的渲染逻辑从渲染器中分离出来,在 Teleport 组件中实现其渲染逻辑。这么做有以下两点好处:
可以避免渲染器逻辑代码 “膨胀”;
当用户没有使用 Teleport 组件时,由于 Teleport 的渲染逻辑被分离,因此可以利用 Tree-Shaking 机制在最终的 bundle 中删除 Teleport 相关的代码,使得最终构建包的体积变小。
patch 函数中对 process 函数的调用如下:
// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 省略部分代码
const { type, ref, shapeFlag } = n2
switch (type) {
// 省略部分代码
default:
// 省略部分代码
// shapeFlag 的类型为 TELEPORT,则它是 Teleport 组件
// 调用 Teleport 组件选项中的 process 函数将控制权交接出去
// 传递给 process 函数的第五个参数是渲染器的一些内部方法
else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
// 省略部分代码
}
// 省略部分代码
}
从上面的源码中可以看到,我们通过vnode 的 shapeFlag 来判断组件是否是 Teleport 组件。如果是,则直接调用组件选项中定义的 process 函数将渲染控制权完全交接出去,这样就实现了渲染逻辑的分离。
Teleport 组件的挂载
// packages/runtime-core/src/components/Teleport.ts
if (n1 == null) {
// 首次渲染 Teleport
// insert anchors in the main view
// 往 container 中插入 Teleport 的注释
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)
// 获取容器,即挂载点
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})`)
}
// 将 n2.children 渲染到指定挂载点
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) {
// 调用渲染器内部的 mountChildren 方法渲染 Teleport 组件的插槽内容
mountChildren(
children as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
// 挂载 Teleport
if (disabled) {
// 如果 Teleport 组件的 disabled 为 true,说明禁用了 <teleport> 的功能,Teleport 只会在 container 中渲染
mount(container, mainAnchor)
} else if (target) {
// 如果没有禁用 <teleport> 的功能,并且存在挂载点,则将其插槽内容渲染到target容中
mount(target, targetAnchor)
}
}
从上面的源码中可以看到,如果旧的虚拟节点 (n1) 不存在,则执行 Teleport 组件的挂载。然后调用 resolveTarget 函数,根据 props.to 属性的值来取得真正的挂载点。
如果没有禁用 的功能 (disabled 为 false ),则调用渲染器内部的 mountChildren 方法将 Teleport 组件挂载到目标元素中。如果 的功能被禁用,则 Teleport 组件将会在周围父组件中指定了 的位置渲染。
Teleport 组件的更新
Teleport 组件在更新时需要考虑多种情况,如下面的代码所示:
// packages/runtime-core/src/components/Teleport.ts
else {
// 更新 Teleport 组件
// update content
n2.el = n1.el
const mainAnchor = (n2.anchor = n1.anchor)!
// 挂载点
const target = (n2.target = n1.target)!
// 锚点
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
// 判断 Teleport 组件是否禁用了
const wasDisabled = isTeleportDisabled(n1.props)
// 如果禁用了 <teleport> 的功能,那么挂载点就是周围父组件,否则就是 to 指定的目标挂载点
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
// 目标挂载点是否是 SVG 标签元素
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.
// 确保所有根级节点在移动之前可以继承之前的 DOM 引用,以便它们在未来的补丁中移动
traverseStaticChildren(n1, n2, true)
} else if (!optimized) {
// 更新子节点
patchChildren(
n1,
n2,
currentContainer,
currentAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
false
)
}
// 如果禁用了 <teleport> 的功能
if (disabled) {
if (!wasDisabled) {
// enabled -> disabled
// move into main container
// 将 Teleport 移动到container容器中
moveTeleport(
n2,
container,
mainAnchor,
internals,
TeleportMoveTypes.TOGGLE
)
}
} else {
// 没有禁用 <teleport> 的功能,判断 to 是否发生变化
// target changed
// 如果新旧 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
// move into teleport target
//
moveTeleport(
n2,
target,
targetAnchor,
internals,
TeleportMoveTypes.TOGGLE
)
}
}
}
如果 Teleport 组件的子节点中有动态子节点,则调用 patchBlockChildren 函数来更新子节点,否则就调用 patchChildren 函数来更新子节点。
接下来判断 Teleport 的功能是否被禁用。如果被禁用了,即 Teleport 组件的 disabled 属性为 true,此时 Teleport 组件只会在周围父组件中指定了 的位置渲染。
如果没有被禁用,那么需要判断 Teleport 组件的 to 属性值是否发生变化。如果发生变化,则需要获取新的挂载点,然后调用 moveTeleport 函数将Teleport组件挂载到到新的挂载点中。如果没有发生变化,则 Teleport 组件将会挂载到先的挂载点中。
moveTeleport 移动Teleport 组件
// packages/runtime-core/src/components/Teleport.ts
function moveTeleport(
vnode: VNode,
container: RendererElement,
parentAnchor: RendererNode | null,
{ o: { insert }, m: move }: RendererInternals,
moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
// move target anchor if this is a target change.
// 插入到目标容器中
if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
insert(vnode.targetAnchor!, container, parentAnchor)
}
const { el, anchor, shapeFlag, children, props } = vnode
const isReorder = moveType === TeleportMoveTypes.REORDER
// move main view anchor if this is a re-order.
if (isReorder) {
// 插入到目标容器中
insert(el!, container, parentAnchor)
}
// if this is a re-order and teleport is enabled (content is in target)
// do not move children. So the opposite is: only move children if this
// is not a reorder, or the teleport is disabled
if (!isReorder || isTeleportDisabled(props)) {
// Teleport has either Array children or no children.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 遍历子节点
for (let i = 0; i < (children as VNode[]).length; i++) {
// 调用 渲染器的黑布方法 move将子节点移动到目标元素中
move(
(children as VNode[])[i],
container,
parentAnchor,
MoveType.REORDER
)
}
}
}
// move main view anchor if this is a re-order.
if (isReorder) {
// 插入到目标容器中
insert(anchor!, container, parentAnchor)
}
}
从上面的源码中可以看到,将 Teleport 组件移动到目标挂载点中,实际上就是调用渲染器的内部方法 insert 和 move 来实现子节点的插入和移动。
hydrateTeleport 服务端渲染 Teleport 组件
hydrateTeleport 函数用于在服务器端渲染 Teleport 组件,其源码如下:
// packages/runtime-core/src/components/Teleport.ts
// 服务端渲染 Teleport
function hydrateTeleport(
node: Node,
vnode: TeleportVNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
slotScopeIds: string[] | null,
optimized: boolean,
{
o: { nextSibling, parentNode, querySelector }
}: RendererInternals<Node, Element>,
hydrateChildren: (
node: Node | null,
vnode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
slotScopeIds: string[] | null,
optimized: boolean
) => Node | null
): Node | null {
// 获取挂载点
const target = (vnode.target = resolveTarget<Element>(
vnode.props,
querySelector
))
if (target) {
// if multiple teleports rendered to the same target element, we need to
// pick up from where the last teleport finished instead of the first node
const targetNode =
(target as TeleportTargetElement)._lpa || target.firstChild
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// <teleport> 的功能被禁用,将 Teleport 渲染到父组件中指定了 <teleport> 的位置
if (isTeleportDisabled(vnode.props)) {
vnode.anchor = hydrateChildren(
nextSibling(node),
vnode,
parentNode(node)!,
parentComponent,
parentSuspense,
slotScopeIds,
optimized
)
vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)
// 将 Teleport 渲染到目标容器中
vnode.targetAnchor = hydrateChildren(
targetNode,
vnode,
target,
parentComponent,
parentSuspense,
slotScopeIds,
optimized
)
}
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
}
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
}
可以看到,在服务端渲染 Teleport 组件时,调用的是服务端渲染的 hydrateChildren 函数来渲染Teleport的内容。如果 的功能被禁用,将 Teleport 渲染到父组件中指定了 的位置,否则将 Teleport 渲染到目标容器target中。
以上就是Vue3之Teleport组件怎么使用的详细内容,更多请关注编程网其它相关文章!