Vue内置组件Teleport的使用
背景
当我们想在 vue 中开发一个能够指定位置渲染的组件例如 tooltip、modal 时可能首先想到的是去引 ui 库中的组件,或者自己手写一个,但 vue3 中提供的内置组件 Teleport 能够帮我们轻松解决问题,下面就来介绍下它的用法以及实现原理
正文
官网对于它的介绍是: <Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
用法及属性
<Teleport to="body" :disabled="disabled"> <div></div> </Teleport> //to: 传送的目标,dom对象/css选择器字符串 //disabled: 是否禁用
const disabled = ref<boolean>(false)
用法还是挺简单的,然后来看下需要注意的地方
Tip
- <Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。如果目标元素也是由 Vue 渲染的,需要确保在挂载 <Teleport> 之前先挂载该元素
- 可以搭配组件使用,只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系,<Teleport> 和内部组件始终保持父子关系,也就是说 props 和 provide 都可以正常使用
- 多个 Teleport 会共享目标,多个 <Teleport> 组件可以将其内容挂载在同一个目标元素上,而顺序就是顺次追加
<Teleport to=".modal"> <div>A</div> </Teleport> <Teleport to=".modal"> <div>B</div> </Teleport>
结果:
<div class=".modal">
<div>A</div>
<div>B</div>
</div>
实现一个简单的 tooltip
<div v-for="(item, index) in array" :key="index" @mousemove="handleMousemove($event, index)" @mouseleave="handleMouseleave" ></div> <teleport to="body"> <div class="max-w-60vw rounded-10px p-10px border-2px fixed border-indigo-300 bg-white" ref="toolTipRef" :style="{ left: tooltipStyle.x, top: tooltipStyle.y, opacity: tooltipStyle.opacity, }" > <span>{ tooltipStyle.content }</span> </div> </teleport>
实现的大概逻辑就是鼠标目标元素上划过时更改 teleport 内部元素的透明度,移除时将透明度改为 0
<script lang="ts" setup> const tooltipStyle = reactive({ x: '0px', y: '0px', content: '', opacity: 0, }); const array = ref<string[]>(['test']) const handleMousemove = (e: MouseEvent, index: number) => { tooltipStyle.opacity = 1; tooltipStyle.x = e.x + 10 + 'px'; tooltipStyle.y = e.y + 10 + 'px'; tooltipStyle.content = array.value[index] }; const handleMouseleave = () => { tooltipStyle.opacity = 0; }; </script>
看完用法,接着就到本文的重点了,让我们来探究下核心源码是怎么实现的
原理
首先我们可以考虑的问题是将 teleport 的渲染和正常 vnode 的渲染分离开,这样做的优点是:
- 渲染函数中保持整洁
- 当我们没有使用 Teleport 时,因为将这个渲染逻辑单独抽出来,所以可以利用 tree-shaking 将相关的代码删除。所以 vue 的做法是针对 teleport 组件重新写了套渲染代码:
在 renderer 的 patch 函数中,如果遇到类型是 teleport 的,就使用自己的挂载方法,这里的 TeleportImpl 就是具体的实现对象
else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process( n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) }
移动函数move
if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals) return }
下面我们来看具体是怎么实现的(保留核心代码)
对应的位置在仓库的Teleport.ts文件中
const TeleportImpl = { process( n1: TeleportVNode | null, n2: TeleportVNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, internals: RendererInternals ) { const { mc: mountChildren, pc: patchChildren, pbc: patchBlockChildren, o: { insert, querySelector, createText, createComment }, } = internals const disabled = isTeleportDisabled(n2.props) let { shapeFlag, children, dynamicChildren } = n2 //如果是首次挂载 if (n1 == null) { // insert anchors in the main view const placeholder = (n2.el = createText('')) const mainAnchor = (n2.anchor = createText('')) insert(placeholder, container, anchor) insert(mainAnchor, container, anchor) //resolveTarget处理传入的to属性 const target = (n2.target = resolveTarget(n2.props, querySelector)) const targetAnchor = (n2.targetAnchor = createText('')) if (target) { insert(targetAnchor, 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) { mountChildren(children as VNodeArrayChildren, container, anchor, parentComponent) } } if (disabled) { mount(container, mainAnchor) } else if (target) { mount(target, targetAnchor) } } else { // 更新 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 if (dynamicChildren) { // fast path when the teleport happens to be a block root patchBlockChildren(n1.dynamicChildren!, dynamicChildren, currentContainer, parentComponent) // 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) } if (disabled) { if (!wasDisabled) { // enabled -> disabled // move into main container moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE) } } else { // target changed 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) { // disabled -> enabled // move into teleport target moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE) } } } }, }
然后我们看 process 里的重要函数
moveTarget
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((children as VNode[])[i], container, parentAnchor, MoveType.REORDER) } } } // move main view anchor if this is a re-order. if (isReorder) { insert(anchor!, container, parentAnchor) } }
resolveTarget
const resolveTarget = <T = RendererElement>( props: TeleportProps | null, select: RendererOptions['querySelector'] ): T | null => { const targetSelector = props && props.to if (isString(targetSelector)) { if (!select) { return null } else { const target = select(targetSelector) return target as any } } else { return targetSelector as any } }
看起来有些复杂,不过让我们理一下思路:上面我们提到了要实现自己的渲染方法,所以我们可以先写基本的渲染函数,然后在内部需要区分是首次挂载还是组件更新,但如果是 teleport 的接收的参数更改了呢,所以这时候就要去主动实现一个 move 函数将内容移动到新的节点下。好了,有了思路后我们来尝试写出来
先来实现组件的挂载与更新
const Teleport = { __isTeleport: true, process(n1, n2, container, anchor, internals) { //通过internals拿到渲染器内部方法 const { patch, patchChildren } = internals // 如果oldVnode不存在,就是全新挂载 if (!n1) { //mount //获取挂载点 const target = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to //将newVnode挂载 n2.children.forEach((child) => patch(null, child, target, anchor)) } else { //更新 patchChildren(n1, n2, container) } }, }
上面我们提到了更新,但如果是 to 属性更改了呢,所以需要有个分支来处理
//如果新旧 to 参数不同,需要对内容移动 if (n2.props.to !== n1.props.to) { //获取新容器 const newTarget = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to //移动到新的容器上 n2.children.forEach((child) => move(child, newTarget)) }
传入 move 函数
else if (shapeFlag & ShapeFlags.TELEPORT) { type.process(n1, n2, container, anchor, internals) { patch, patchChildren, move(vnode, container, anchor) { //这里只处理了组件或者普通元素 const el = vnode.component ? vnode.component.subTree.el : vnode.el const { insert } = internals insert(el, container, anchor) } } }
这里我们省略了处理 disabled 的 remove 函数,不是本文研究的重点,具体可以看源码的 Teleport 文件
到此这篇关于Vue内置组件Teleport的使用的文章就介绍到这了,更多相关Vue Teleport内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
VUE的history模式下除了index外其他路由404报错解决办法
在本篇文章里小编给大家分享的是关于VUE的history模式下除了index外其他路由404报错解决办法,对此有需要的朋友们可以学习下。2019-08-08
最新评论