詳解Vue3 Teleport 的實踐及原理
Vue3 的組合式 API 以及基於 Proxy 響應式原理已經有很多文章介紹過了,除了這些比較亮眼的更新,Vue3 還新增了一個內建元件: Teleport 。這個元件的作用主要用來將模板內的 DOM 元素移動到其他位置。
使用場景
業務開發的過程中,我們經常會封裝一些常用的元件,例如 Modal 元件。相信大家在使用 Modal 元件的過程中,經常會遇到一個問題,那就是 Modal 的定位問題。
話不多說,我們先寫一個簡單的 Modal 元件。
<!-- Modal.vue --> <style lang="scss"> .modal { &__mask { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0.5); } &__main { margin: 0 auto; margin-bottom: 5%; margin-top: 20%; width: 500px; background: #fff; border-radius: 8px; } /* 省略部分樣式 */ } </style> <template> <div class="modal__mask"> <div class="modal__main"> <div class="modal__header"> <h3 class="modal__title">彈窗標題</h3> <span class="modal__close">x</span> </div> <div class="modal__content"> 彈窗文字內容 </div> <div class="modal__footer"> <button>取消</button> <button>確認</button> </div> </div> </div> </template> <script> export default { setup() { return {}; },}; </script>
然後我們在頁面中引入 Modal 元件。
<!-- App.vue --> <style lang="scss"> .container { height: 80vh; margin: 50px; overflow: hidden; } </style> <template> <div class="container"> <Modal /> </div> </template> <script> export default { components: { Modal,},setup() { return {}; } }; </script>
如上圖所示, div.container
下彈窗元件正常展示。使用 fixed
進行佈局的元素,在一般情況下會相對於螢幕視窗來進行定位,但是如果父元素的 transform
,perspective
或 filter
屬性不為 none
時, fixed
元素就會相對於父元素來進行定位。
我們只需要把 .container
類的 transform
稍作修改,彈窗元件的定位就會錯亂。
<style lang="scss"> .container { height: 80vh; margin: 50px; overflow: hidden; transform: translateZ(0); } </style>
這個時候,使用 Teleport
元件就能解決這個問題了。
Teleport 提供了一種乾淨的方法,允許我們控制在 DOM 中哪個父節點下呈現 HTML,而不必求助於全域性狀態或將其拆分為兩個元件。 -- Vue 官方文件
我們只需要將彈窗內容放入 Teleport
內,並設定 to
屬性為 body
,表示彈窗元件每次渲染都會做為 body
的子級,這樣之前的問題就能得到解決。
<template> <teleport to="body"> <div class="modal__mask"> <div class="modal__main"> ... </div> </div> </teleport> </template>
可以在 https://codesandbox.io/embed/vue-modal-h5g8y 檢視程式碼。
原始碼解析
我們可以先寫一個簡單的模板,然後看看 Teleport
元件經過模板編譯後,生成的程式碼。
Vue.createApp({ template: ` <Teleport to="body"> <div> teleport to body </div> </Teleport> ` })
簡化後代碼:
function render(_ctx,_cache) { with (_ctx) { const { createVNode,openBlock,createBlock,Teleport } = Vue return (openBlock(),createBlock(Teleport,{ to: "body" },[ createVNode("div",null," teleport to body ",-1 /* HOISTED */) ])) } }
可以看到 Teleport
元件通過 createBlock
進行建立。
// packages/runtime-core/src/renderer.ts export function createBlock( type,props,children,patchFlag ) { const vnode = createVNode( type,patchFlag ) // ... 省略部分邏輯 return vnode } export function createVNode( type,patchFlag ) { // class & style normalization. if (props) { // ... } // encode the vnode type information into a bitmap const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 const vnode: VNode = { type,shapeFlag,patchFlag,key: props && normalizeKey(props),ref: props && normalizeRef(props),} return vnode } // packages/runtime-core/src/components/Teleport.ts export const isTeleport = type => type.__isTeleport export const Teleport = { __isTeleport: true,process() {} }
傳入 createBlock
的第一個引數為 Teleport
,最後得到的 vnode 中會有一個 shapeFlag
屬性,該屬性用來表示 vnode 的型別。 isTeleport(type)
得到的結果為 true
,所以 shapeFlag
屬性最後的值為 ShapeFlags.TELEPORT
( 1 << 6
)。
// packages/shared/src/shapeFlags.ts export const enum ShapeFlags { ELEMENT = 1,FUNCTIONAL_COMPONENT = 1 << 1,STATEFUL_COMPONENT = 1 << 2,TEXT_CHILDREN = 1 << 3,ARRAY_CHILDREN = 1 << 4,SLOTS_CHILDREN = 1 << 5,TELEPORT = 1 << 6,SUSPENSE = 1 << 7,COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,COMPONENT_KEPT_ALIVE = 1 << 9 }
在元件的 render 節點,會依據 type
和 shapeFlag
走不同的邏輯。
// packages/runtime-core/src/renderer.ts const render = (vnode,container) => { if (vnode == null) { // 當前元件為空,則將元件銷燬 if (container._vnode) { unmount(container._vnode,true) } } else { // 新建或者更新元件 // container._vnode 是之前已建立元件的快取 patch(container._vnode || null,vnode,container) } container._vnode = vnode } // patch 是表示補丁,用於 vnode 的建立、更新、銷燬 const patch = (n1,n2,container) => { // 如果新舊節點的型別不一致,則將舊節點銷燬 if (n1 && !isSameVNodeType(n1,n2)) { unmount(n1) } const { type,ref,shapeFlag } = n2 switch (type) { case Text: // 處理文字 break case Comment: // 處理註釋 break // case ... default: if (shapeFlag & ShapeFlags.ELEMENT) { // 處理 DOM 元素 } else if (shapeFlag & ShapeFlags.COMPONENT) { // 處理自定義元件 } else if (shapeFlag & ShapeFlags.TELEPORT) { // 處理 Teleport 元件 // 呼叫 Teleport.process 方法 type.process(n1,container...); } // else if ... } }
可以看到,在處理 Teleport
時,最後會呼叫 Teleport.process
方法,Vue3 中很多地方都是通過 process 的方式來處理 vnode 相關邏輯的,下面我們重點看看 Teleport.process
方法做了些什麼。
// packages/runtime-core/src/components/Teleport.ts const isTeleportDisabled = props => props.disabled export const Teleport = { __isTeleport: true,process(n1,container) { const disabled = isTeleportDisabled(n2.props) const { shapeFlag,children } = n2 if (n1 == null) { const target = (n2.target = querySelector(n2.prop.to)) const mount = (container) => { // compiler and vnode children normalization. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(children,container) } } if (disabled) { // 開關關閉,掛載到原來的位置 mount(container) } else if (target) { // 將子節點,掛載到屬性 `to` 對應的節點上 mount(target) } } else { // n1不存在,更新節點即可 } } }
其實原理很簡單,就是將 Teleport
的 children
掛載到屬性 to
對應的 DOM 元素中。為了方便理解,這裡只是展示了原始碼的九牛一毛,省略了很多其他的操作。
總結
希望在閱讀文章的過程中,大家能夠掌握 Teleport
元件的用法,並使用到業務場景中。儘管原理十分簡單,但是我們有了 Teleport
元件,就能輕鬆解決彈窗元素定位不準確的問題。
到此這篇關於詳解Vue3 Teleport 的實踐及原理的文章就介紹到這了,更多相關Vue3 Teleport元件內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!