这篇文章主要介绍了Vue3之Teleport组件如何使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue3之Teleport组件如何使用文章都会有所收获,下面我们一起来看看吧。
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中。