Vue3 Teleport 组件的实践及原理

开发 前端
Vue3 的组合式 API 以及基于 Proxy 响应式原理已经有很多文章介绍过了,除了这些比较亮眼的更新,Vue3 还新增了一个内置组件:Teleport。这个组件的作用主要用来将模板内的 DOM 元素移动到其他位置。

[[354904]]

Vue3 的组合式 API 以及基于 Proxy 响应式原理已经有很多文章介绍过了,除了这些比较亮眼的更新,Vue3 还新增了一个内置组件:Teleport。这个组件的作用主要用来将模板内的 DOM 元素移动到其他位置。

使用场景

业务开发的过程中,我们经常会封装一些常用的组件,例如 Modal 组件。相信大家在使用 Modal 组件的过程中,经常会遇到一个问题,那就是 Modal 的定位问题。

话不多说,我们先写一个简单的 Modal 组件。

  1. <!-- Modal.vue --> 
  2. <style lang="scss"
  3. .modal { 
  4.   &__mask { 
  5.     position: fixed; 
  6.     top: 0; 
  7.     left: 0; 
  8.     width: 100vw; 
  9.     height: 100vh; 
  10.     background: rgba(0, 0, 0, 0.5); 
  11.   } 
  12.   &__main { 
  13.     margin: 0 auto; 
  14.     margin-bottom: 5%; 
  15.     margin-top: 20%; 
  16.     width: 500px; 
  17.     background: #fff; 
  18.     border-radius: 8px; 
  19.   } 
  20.   /* 省略部分样式 */ 
  21. </style> 
  22. <template> 
  23.   <div class="modal__mask"
  24.     <div class="modal__main"
  25.       <div class="modal__header"
  26.         <h3 class="modal__title">弹窗标题</h3> 
  27.         <span class="modal__close">x</span> 
  28.       </div> 
  29.       <div class="modal__content"
  30.         弹窗文本内容 
  31.       </div> 
  32.       <div class="modal__footer"
  33.         <button>取消</button> 
  34.         <button>确认</button> 
  35.       </div> 
  36.     </div> 
  37.   </div> 
  38. </template> 
  39.  
  40. <script> 
  41. export default { 
  42.   setup() { 
  43.     return {}; 
  44.   }, 
  45. }; 
  46. </script> 

然后我们在页面中引入 Modal 组件。

  1. <!-- App.vue --> 
  2. <style lang="scss"
  3. .container { 
  4.   height: 80vh; 
  5.   margin: 50px; 
  6.   overflow: hidden; 
  7. </style> 
  8. <template> 
  9.   <div class="container"
  10.     <Modal /> 
  11.   </div> 
  12. </template> 
  13.  
  14. <script> 
  15. export default { 
  16.   components: { 
  17.     Modal, 
  18.   }, 
  19.   setup() { 
  20.     return {}; 
  21.   } 
  22. }; 
  23. </script> 

Modal

如上图所示, div.container 下弹窗组件正常展示。使用 fixed 进行布局的元素,在一般情况下会相对于屏幕视窗来进行定位,但是如果父元素的 transform, perspective 或 filter 属性不为 none 时,fixed 元素就会相对于父元素来进行定位。

我们只需要把 .container 类的 transform 稍作修改,弹窗组件的定位就会错乱。

  1. <style lang="scss"
  2. .container { 
  3.   height: 80vh; 
  4.   margin: 50px; 
  5.   overflow: hidden; 
  6.   transform: translateZ(0); 
  7. </style> 

Modal

这个时候,使用 Teleport 组件就能解决这个问题了。

“Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。-- Vue 官方文档

我们只需要将弹窗内容放入 Teleport 内,并设置 to 属性为 body,表示弹窗组件每次渲染都会做为 body 的子级,这样之前的问题就能得到解决。

  1. <template> 
  2.   <teleport to="body"
  3.     <div class="modal__mask"
  4.       <div class="modal__main"
  5.         ... 
  6.       </div> 
  7.     </div> 
  8.   </teleport> 
  9. </template> 

可以在 https://codesandbox.io/embed/vue-modal-h5g8y 查看代码。

使用 Teleport 的 Modal

源码解析

我们可以先写一个简单的模板,然后看看 Teleport 组件经过模板编译后,生成的代码。

  1. Vue.createApp({ 
  2.   template: ` 
  3.     <Teleport to="body"
  4.       <div> teleport to body </div>   
  5.     </Teleport> 
  6.   ` 
  7. }) 

模板编译后的代码

简化后代码:

  1. function render(_ctx, _cache) { 
  2.   with (_ctx) { 
  3.     const { createVNode, openBlock, createBlock, Teleport } = Vue 
  4.     return (openBlock(), createBlock(Teleport, { to"body" }, [ 
  5.       createVNode("div"null" teleport to body ", -1 /* HOISTED */) 
  6.     ])) 
  7.   } 

可以看到 Teleport 组件通过 createBlock 进行创建。

  1. // packages/runtime-core/src/renderer.ts 
  2. export function createBlock( 
  3.  type, props, children, patchFlag 
  4. ) { 
  5.   const vnode = createVNode( 
  6.     type, 
  7.     props, 
  8.     children, 
  9.     patchFlag 
  10.   ) 
  11.   // ... 省略部分逻辑 
  12.   return vnode 
  13.  
  14. export function createVNode( 
  15.   type, props, children, patchFlag 
  16. ) { 
  17.   // class & style normalization. 
  18.   if (props) { 
  19.     // ... 
  20.   } 
  21.  
  22.   // encode the vnode type information into a bitmap 
  23.   const shapeFlag = isString(type) 
  24.     ? ShapeFlags.ELEMENT 
  25.     : __FEATURE_SUSPENSE__ && isSuspense(type) 
  26.       ? ShapeFlags.SUSPENSE 
  27.       : isTeleport(type) 
  28.         ? ShapeFlags.TELEPORT 
  29.         : isObject(type) 
  30.           ? ShapeFlags.STATEFUL_COMPONENT 
  31.           : isFunction(type) 
  32.             ? ShapeFlags.FUNCTIONAL_COMPONENT 
  33.             : 0 
  34.  
  35.   const vnode: VNode = { 
  36.     type, 
  37.     props, 
  38.     shapeFlag, 
  39.     patchFlag, 
  40.     key: props && normalizeKey(props), 
  41.     ref: props && normalizeRef(props), 
  42.   } 
  43.  
  44.   return vnode 
  45.  
  46. // packages/runtime-core/src/components/Teleport.ts 
  47. export const isTeleport = type => type.__isTeleport 
  48. export const Teleport = { 
  49.   __isTeleport: true
  50.   process() {} 

传入 createBlock 的第一个参数为 Teleport,最后得到的 vnode 中会有一个 shapeFlag 属性,该属性用来表示 vnode 的类型。isTeleport(type) 得到的结果为 true,所以 shapeFlag 属性最后的值为 ShapeFlags.TELEPORT(1 << 6)。

  1. // packages/shared/src/shapeFlags.ts 
  2. export const enum ShapeFlags { 
  3.   ELEMENT = 1, 
  4.   FUNCTIONAL_COMPONENT = 1 << 1, 
  5.   STATEFUL_COMPONENT = 1 << 2, 
  6.   TEXT_CHILDREN = 1 << 3, 
  7.   ARRAY_CHILDREN = 1 << 4, 
  8.   SLOTS_CHILDREN = 1 << 5, 
  9.   TELEPORT = 1 << 6, 
  10.   SUSPENSE = 1 << 7, 
  11.   COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, 
  12.   COMPONENT_KEPT_ALIVE = 1 << 9 

在组件的 render 节点,会依据 type 和 shapeFlag 走不同的逻辑。

  1. // packages/runtime-core/src/renderer.ts 
  2. const render = (vnode, container) => { 
  3.   if (vnode == null) { 
  4.     // 当前组件为空,则将组件销毁 
  5.     if (container._vnode) { 
  6.       unmount(container._vnode, nullnulltrue
  7.     } 
  8.   } else { 
  9.     // 新建或者更新组件 
  10.     // container._vnode 是之前已创建组件的缓存 
  11.     patch(container._vnode || null, vnode, container) 
  12.   } 
  13.   container._vnode = vnode 
  14.  
  15. // patch 是表示补丁,用于 vnode 的创建、更新、销毁 
  16. const patch = (n1, n2, container) => { 
  17.   // 如果新旧节点的类型不一致,则将旧节点销毁 
  18.   if (n1 && !isSameVNodeType(n1, n2)) { 
  19.     unmount(n1) 
  20.   } 
  21.   const { type, ref, shapeFlag } = n2 
  22.   switch (type) { 
  23.     case Text: 
  24.       // 处理文本 
  25.       break 
  26.     case Comment: 
  27.       // 处理注释 
  28.       break 
  29.     // case ... 
  30.     default
  31.       if (shapeFlag & ShapeFlags.ELEMENT) { 
  32.         // 处理 DOM 元素 
  33.       } else if (shapeFlag & ShapeFlags.COMPONENT) { 
  34.         // 处理自定义组件 
  35.       } else if (shapeFlag & ShapeFlags.TELEPORT) { 
  36.         // 处理 Teleport 组件 
  37.         // 调用 Teleport.process 方法 
  38.         type.process(n1, n2, container...); 
  39.       } // else if ... 
  40.   } 

可以看到,在处理 Teleport 时,最后会调用 Teleport.process 方法,Vue3 中很多地方都是通过 process 的方式来处理 vnode 相关逻辑的,下面我们重点看看 Teleport.process 方法做了些什么。

  1. // packages/runtime-core/src/components/Teleport.ts 
  2. const isTeleportDisabled = props => props.disabled 
  3. export const Teleport = { 
  4.   __isTeleport: true
  5.   process(n1, n2, container) { 
  6.     const disabled = isTeleportDisabled(n2.props) 
  7.     const { shapeFlag, children } = n2 
  8.     if (n1 == null) { 
  9.       const target = (n2.target = querySelector(n2.prop.to))       
  10.       const mount = (container) => { 
  11.         // compiler and vnode children normalization. 
  12.         if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 
  13.           mountChildren(children, container) 
  14.         } 
  15.       } 
  16.       if (disabled) { 
  17.         // 开关关闭,挂载到原来的位置 
  18.         mount(container) 
  19.       } else if (target) { 
  20.         // 将子节点,挂载到属性 `to` 对应的节点上 
  21.         mount(target) 
  22.       } 
  23.     } 
  24.     else { 
  25.       // n1不存在,更新节点即可 
  26.     } 
  27.   } 

其实原理很简单,就是将 Teleport 的 children 挂载到属性 to 对应的 DOM 元素中。为了方便理解,这里只是展示了源码的九牛一毛,省略了很多其他的操作。

总结

希望在阅读文章的过程中,大家能够掌握 Teleport 组件的用法,并使用到业务场景中。尽管原理十分简单,但是我们有了 Teleport 组件,就能轻松解决弹窗元素定位不准确的问题。

本文转载自微信公众号「更了不起的前端」,可以通过以下二维码关注。转载本文请联系更了不起的前端公众号。

 

 

 

责任编辑:武晓燕 来源: 更了不起的前端
相关推荐

2021-05-12 10:25:29

开发技能代码

2021-10-29 07:47:35

Vue 3teleport传送门组件

2021-03-31 08:01:50

Vue3 Vue2 Vue3 Telepo

2023-11-28 09:03:59

Vue.jsJavaScript

2024-10-24 09:18:45

2021-05-18 07:51:37

Suspense组件Vue3

2023-04-02 10:06:24

组件vue3sign2.

2022-07-29 11:03:47

VueUni-app

2023-11-29 09:05:59

Vue 3场景

2023-04-27 11:07:24

Setup语法糖Vue3

2024-04-08 07:28:27

PiniaVue3状态管理库

2022-09-20 11:00:14

Vue3滚动组件

2021-12-01 08:11:44

Vue3 插件Vue应用

2024-08-13 09:26:07

2022-07-27 08:40:06

父子组件VUE3

2021-11-30 08:19:43

Vue3 插件Vue应用

2021-10-21 06:52:17

Vue3组件 API

2024-04-16 07:46:15

Vue3STOMP协议WebSocket

2021-11-19 09:29:25

项目技术开发

2021-09-27 06:29:47

Vue3 响应式原理Vue应用
点赞
收藏

51CTO技术栈公众号