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

