Vue3 Teleport 組件的實(shí)踐及原理
Vue3 的組合式 API 以及基于 Proxy 響應(yīng)式原理已經(jīng)有很多文章介紹過了,除了這些比較亮眼的更新,Vue3 還新增了一個(gè)內(nèi)置組件:Teleport。這個(gè)組件的作用主要用來將模板內(nèi)的 DOM 元素移動(dòng)到其他位置。
使用場景
業(yè)務(wù)開發(fā)的過程中,我們經(jīng)常會(huì)封裝一些常用的組件,例如 Modal 組件。相信大家在使用 Modal 組件的過程中,經(jīng)常會(huì)遇到一個(gè)問題,那就是 Modal 的定位問題。
話不多說,我們先寫一個(gè)簡單的 Modal 組件。
- <!-- 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">彈窗標(biāo)題</h3>
- <span class="modal__close">x</span>
- </div>
- <div class="modal__content">
- 彈窗文本內(nèi)容
- </div>
- <div class="modal__footer">
- <button>取消</button>
- <button>確認(rèn)</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>
Modal
如上圖所示, div.container 下彈窗組件正常展示。使用 fixed 進(jìn)行布局的元素,在一般情況下會(huì)相對(duì)于屏幕視窗來進(jìn)行定位,但是如果父元素的 transform, perspective 或 filter 屬性不為 none 時(shí),fixed 元素就會(huì)相對(duì)于父元素來進(jìn)行定位。
我們只需要把 .container 類的 transform 稍作修改,彈窗組件的定位就會(huì)錯(cuò)亂。
- <style lang="scss">
- .container {
- height: 80vh;
- margin: 50px;
- overflow: hidden;
- transform: translateZ(0);
- }
- </style>
Modal
這個(gè)時(shí)候,使用 Teleport 組件就能解決這個(gè)問題了。
“Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個(gè)父節(jié)點(diǎn)下呈現(xiàn) HTML,而不必求助于全局狀態(tài)或?qū)⑵洳鸱譃閮蓚€(gè)組件。-- Vue 官方文檔
我們只需要將彈窗內(nèi)容放入 Teleport 內(nèi),并設(shè)置 to 屬性為 body,表示彈窗組件每次渲染都會(huì)做為 body 的子級(jí),這樣之前的問題就能得到解決。
- <template>
- <teleport to="body">
- <div class="modal__mask">
- <div class="modal__main">
- ...
- </div>
- </div>
- </teleport>
- </template>
可以在 https://codesandbox.io/embed/vue-modal-h5g8y 查看代碼。
使用 Teleport 的 Modal
源碼解析
我們可以先寫一個(gè)簡單的模板,然后看看 Teleport 組件經(jīng)過模板編譯后,生成的代碼。
- 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 進(jìn)行創(chuàng)建。
- // 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 的第一個(gè)參數(shù)為 Teleport,最后得到的 vnode 中會(huì)有一個(gè) shapeFlag 屬性,該屬性用來表示 vnode 的類型。isTeleport(type) 得到的結(jié)果為 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 節(jié)點(diǎn),會(huì)依據(jù) type 和 shapeFlag 走不同的邏輯。
- // packages/runtime-core/src/renderer.ts
- const render = (vnode, container) => {
- if (vnode == null) {
- // 當(dāng)前組件為空,則將組件銷毀
- if (container._vnode) {
- unmount(container._vnode, null, null, true)
- }
- } else {
- // 新建或者更新組件
- // container._vnode 是之前已創(chuàng)建組件的緩存
- patch(container._vnode || null, vnode, container)
- }
- container._vnode = vnode
- }
- // patch 是表示補(bǔ)丁,用于 vnode 的創(chuàng)建、更新、銷毀
- const patch = (n1, n2, container) => {
- // 如果新舊節(jié)點(diǎn)的類型不一致,則將舊節(jié)點(diǎn)銷毀
- 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 組件
- // 調(diào)用 Teleport.process 方法
- type.process(n1, n2, container...);
- } // else if ...
- }
- }
可以看到,在處理 Teleport 時(shí),最后會(huì)調(diào)用 Teleport.process 方法,Vue3 中很多地方都是通過 process 的方式來處理 vnode 相關(guān)邏輯的,下面我們重點(diǎn)看看 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) {
- // 開關(guān)關(guān)閉,掛載到原來的位置
- mount(container)
- } else if (target) {
- // 將子節(jié)點(diǎn),掛載到屬性 `to` 對(duì)應(yīng)的節(jié)點(diǎn)上
- mount(target)
- }
- }
- else {
- // n1不存在,更新節(jié)點(diǎn)即可
- }
- }
- }
其實(shí)原理很簡單,就是將 Teleport 的 children 掛載到屬性 to 對(duì)應(yīng)的 DOM 元素中。為了方便理解,這里只是展示了源碼的九牛一毛,省略了很多其他的操作。
總結(jié)
希望在閱讀文章的過程中,大家能夠掌握 Teleport 組件的用法,并使用到業(yè)務(wù)場景中。盡管原理十分簡單,但是我們有了 Teleport 組件,就能輕松解決彈窗元素定位不準(zhǔn)確的問題。
本文轉(zhuǎn)載自微信公眾號(hào)「更了不起的前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系更了不起的前端公眾號(hào)。
.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;
}
/* 省略部分樣式 */
}
.container {
height: 80vh;
margin: 50px;
overflow: hidden;
}