Vue3 的組合式 API 以及基於 Proxy 響應式原理已經有不少文章介紹過了,除了這些比較亮眼的更新,Vue3 還新增了一個內置組件:Teleport。這個組件的做用主要用來將模板內的 DOM 元素移動到其餘位置。css
業務開發的過程當中,咱們常常會封裝一些經常使用的組件,例如 Modal 組件。相信你們在使用 Modal 組件的過程當中,常常會遇到一個問題,那就是 Modal 的定位問題。html
話很少說,咱們先寫一個簡單的 Modal 組件。vue
<!-- Modal.vue --> <style lang="scss"> .modal { &__mask { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 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 組件。node
<!-- 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
稍做修改,彈窗組件的定位就會錯亂。ide
<style lang="scss"> .container { height: 80vh; margin: 50px; overflow: hidden; transform: translateZ(0); } </style>
這個時候,使用 Teleport
組件就能解決這個問題了。佈局
Teleport 提供了一種乾淨的方法,容許咱們控制在 DOM 中哪一個父節點下呈現 HTML,而沒必要求助於全局狀態或將其拆分爲兩個組件。 -- Vue 官方文檔
咱們只須要將彈窗內容放入 Teleport
內,並設置 to
屬性爲 body
,表示彈窗組件每次渲染都會作爲 body
的子級,這樣以前的問題就能獲得解決。ui
<template> <teleport to="body"> <div class="modal__mask"> <div class="modal__main"> ... </div> </div> </teleport> </template>
能夠在 https://codesandbox.io/embed/vue-modal-h5g8y 查看代碼。spa
咱們能夠先寫一個簡單的模板,而後看看 Teleport
組件通過模板編譯後,生成的代碼。code
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, props, children, patchFlag ) // ... 省略部分邏輯 return vnode } export function createVNode( type, props, children, 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, props, 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, null, null, 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, n2, 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, n2, 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
組件,就能輕鬆解決彈窗元素定位不許確的問題。