Vue內(nèi)置組件Teleport的使用
背景
當(dāng)我們想在 vue 中開發(fā)一個(gè)能夠指定位置渲染的組件例如 tooltip、modal 時(shí)可能首先想到的是去引 ui 庫(kù)中的組件,或者自己手寫一個(gè),但 vue3 中提供的內(nèi)置組件 Teleport 能夠幫我們輕松解決問題,下面就來介紹下它的用法以及實(shí)現(xiàn)原理
正文
官網(wǎng)對(duì)于它的介紹是: <Teleport> 是一個(gè)內(nèi)置組件,它可以將一個(gè)組件內(nèi)部的一部分模板“傳送”到該組件的 DOM 結(jié)構(gòu)外層的位置去
用法及屬性
<Teleport to="body" :disabled="disabled"> ? <div></div> </Teleport> //to: 傳送的目標(biāo),dom對(duì)象/css選擇器字符串 //disabled: 是否禁用
const disabled = ref<boolean>(false)
用法還是挺簡(jiǎn)單的,然后來看下需要注意的地方
Tip
- <Teleport> 掛載時(shí),傳送的 to 目標(biāo)必須已經(jīng)存在于 DOM 中。如果目標(biāo)元素也是由 Vue 渲染的,需要確保在掛載 <Teleport> 之前先掛載該元素
- 可以搭配組件使用,只改變了渲染的 DOM 結(jié)構(gòu),它不會(huì)影響組件間的邏輯關(guān)系,<Teleport> 和內(nèi)部組件始終保持父子關(guān)系,也就是說 props 和 provide 都可以正常使用
- 多個(gè) Teleport 會(huì)共享目標(biāo),多個(gè) <Teleport> 組件可以將其內(nèi)容掛載在同一個(gè)目標(biāo)元素上,而順序就是順次追加
<Teleport to=".modal"> <div>A</div> </Teleport> <Teleport to=".modal"> <div>B</div> </Teleport>
結(jié)果:
<div class=".modal">
<div>A</div>
<div>B</div>
</div>
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 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>
實(shí)現(xiàn)的大概邏輯就是鼠標(biāo)目標(biāo)元素上劃過時(shí)更改 teleport 內(nèi)部元素的透明度,移除時(shí)將透明度改為 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>
看完用法,接著就到本文的重點(diǎn)了,讓我們來探究下核心源碼是怎么實(shí)現(xiàn)的
原理
首先我們可以考慮的問題是將 teleport 的渲染和正常 vnode 的渲染分離開,這樣做的優(yōu)點(diǎn)是:
- 渲染函數(shù)中保持整潔
- 當(dāng)我們沒有使用 Teleport 時(shí),因?yàn)閷⑦@個(gè)渲染邏輯單獨(dú)抽出來,所以可以利用 tree-shaking 將相關(guān)的代碼刪除。所以 vue 的做法是針對(duì) teleport 組件重新寫了套渲染代碼:
在 renderer 的 patch 函數(shù)中,如果遇到類型是 teleport 的,就使用自己的掛載方法,這里的 TeleportImpl 就是具體的實(shí)現(xiàn)對(duì)象
else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process( n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) }
移動(dòng)函數(shù)move
if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals) return }
下面我們來看具體是怎么實(shí)現(xiàn)的(保留核心代碼)
對(duì)應(yīng)的位置在倉(cāng)庫(kù)的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 里的重要函數(shù)
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 } }
看起來有些復(fù)雜,不過讓我們理一下思路:上面我們提到了要實(shí)現(xiàn)自己的渲染方法,所以我們可以先寫基本的渲染函數(shù),然后在內(nèi)部需要區(qū)分是首次掛載還是組件更新,但如果是 teleport 的接收的參數(shù)更改了呢,所以這時(shí)候就要去主動(dòng)實(shí)現(xiàn)一個(gè) move 函數(shù)將內(nèi)容移動(dòng)到新的節(jié)點(diǎn)下。好了,有了思路后我們來嘗試寫出來
先來實(shí)現(xiàn)組件的掛載與更新
const Teleport = { ? __isTeleport: true, ? process(n1, n2, container, anchor, internals) { ? ? //通過internals拿到渲染器內(nèi)部方法 ? ? const { patch, patchChildren } = internals ? ? // 如果oldVnode不存在,就是全新掛載 ? ? if (!n1) { ? ? ? //mount ? ? ? //獲取掛載點(diǎn) ? ? ? 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 屬性更改了呢,所以需要有個(gè)分支來處理
//如果新舊 to 參數(shù)不同,需要對(duì)內(nèi)容移動(dòng) if (n2.props.to !== n1.props.to) { //獲取新容器 const newTarget = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to //移動(dòng)到新的容器上 n2.children.forEach((child) => move(child, newTarget)) }
傳入 move 函數(shù)
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 函數(shù),不是本文研究的重點(diǎn),具體可以看源碼的 Teleport 文件
到此這篇關(guān)于Vue內(nèi)置組件Teleport的使用的文章就介紹到這了,更多相關(guān)Vue Teleport內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue3中內(nèi)置組件Teleport的基本使用與典型案例
- Vue中代碼傳送(teleport)的實(shí)現(xiàn)
- Vue3?源碼解讀之?Teleport?組件使用示例
- vue3新增Teleport的問題
- Vue中使用Teleport的方法示例
- vue2如何實(shí)現(xiàn)vue3的teleport
- vue3 teleport的使用案例詳解
- Vue3內(nèi)置組件Teleport使用方法詳解
- vue基于Teleport實(shí)現(xiàn)Modal組件
- 詳解Vue3中Teleport的使用
- vue3 Teleport瞬間移動(dòng)函數(shù)使用方法詳解
- 詳解Vue3 Teleport 的實(shí)踐及原理
相關(guān)文章
Vue3+Vite項(xiàng)目使用mockjs隨機(jī)模擬數(shù)據(jù)
這篇文章主要介紹了Vue3+Vite項(xiàng)目使用mockjs隨機(jī)模擬數(shù)據(jù),需要的朋友可以參考下2023-01-01Vue計(jì)算屬性中reduce方法實(shí)現(xiàn)遍歷方式
這篇文章主要介紹了Vue計(jì)算屬性中reduce方法實(shí)現(xiàn)遍歷方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Vue通過echarts實(shí)現(xiàn)數(shù)據(jù)圖表化顯示
Echarts,它是一個(gè)與框架無關(guān)的 JS 圖表庫(kù),但是它基于Js,這樣很多框架都能使用它,例如Vue,估計(jì)IONIC也能用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08vue+iview/elementUi實(shí)現(xiàn)城市多選
這篇文章主要介紹了vue+iview/elementUi實(shí)現(xiàn)城市多選,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03VUE table表格動(dòng)態(tài)添加一列數(shù)據(jù),新增的這些數(shù)據(jù)不可以編輯(v-model綁定的數(shù)據(jù)不能實(shí)時(shí)更新)
這篇文章主要介紹了VUE table表格動(dòng)態(tài)添加一列數(shù)據(jù),新增的這些數(shù)據(jù)不可以編輯(v-model綁定的數(shù)據(jù)不能實(shí)時(shí)更新),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-04-04vue頁(yè)面切換項(xiàng)目實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫的方法
這篇文章主要介紹了vue頁(yè)面切換項(xiàng)目實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11vue+elementUI實(shí)現(xiàn)動(dòng)態(tài)面包屑
這篇文章主要為大家詳細(xì)介紹了vue+elementUI實(shí)現(xiàn)動(dòng)態(tài)面包屑,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04