详解Vue3 Teleport 的实践及原理


Posted in Vue.js onDecember 02, 2020

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, 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>

详解Vue3 Teleport 的实践及原理

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

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

<style lang="scss">
.container {
 height: 80vh;
 margin: 50px;
 overflow: hidden;
 transform: translateZ(0);
}
</style>

详解Vue3 Teleport 的实践及原理

这个时候,使用 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 查看代码。

详解Vue3 Teleport 的实践及原理

源码解析

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

Vue.createApp({
 template: `
  <Teleport to="body">
   <div> teleport to body </div> 
  </Teleport>
 `
})

详解Vue3 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.TELEPORT1 << 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 节点,会依据 typeshapeFlag 走不同的逻辑。

// 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不存在,更新节点即可
  }
 }
}

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

总结

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

到此这篇关于详解Vue3 Teleport 的实践及原理的文章就介绍到这了,更多相关Vue3 Teleport组件内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
一篇超完整的Vue新手入门指导教程
Nov 18 Vue.js
vue表单验证之禁止input输入框输入空格
Dec 03 Vue.js
vue 使用 sortable 实现 el-table 拖拽排序功能
Dec 26 Vue.js
vue form表单post请求结合Servlet实现文件上传功能
Jan 22 Vue.js
vue实现可拖拽的dialog弹框
May 13 Vue.js
Vue3.0中Ref与Reactive的区别示例详析
Jul 07 Vue.js
Vue实现跑马灯样式文字横向滚动
Nov 23 Vue.js
Vue中使用import进行路由懒加载的原理分析
Apr 01 Vue.js
vue elementUI表格控制对应列
Apr 13 Vue.js
vue实现省市区联动 element-china-area-data插件
Apr 22 Vue.js
详解Vue3使用axios的配置教程
Apr 29 Vue.js
vue中data里面的数据相互使用方式
Jun 05 Vue.js
vue $router和$route的区别详解
Dec 02 #Vue.js
基于vue项目设置resolves.alias: '@'路径并适配webstorm
Dec 02 #Vue.js
element-plus一个vue3.xUI框架(element-ui的3.x 版初体验)
Dec 02 #Vue.js
vue组件中节流函数的失效的原因和解决方法
Dec 02 #Vue.js
Vue3+elementui plus创建项目的方法
Dec 01 #Vue.js
Vue.js桌面端自定义滚动条组件之美化滚动条VScroll
Dec 01 #Vue.js
vue开发chrome插件,实现获取界面数据和保存到数据库功能
Dec 01 #Vue.js
You might like
桌面中心(二)数据库写入
2006/10/09 PHP
php下清空字符串中的HTML标签的代码
2010/09/06 PHP
php数组函数序列之rsort() - 对数组的元素值进行降序排序
2011/11/02 PHP
PHP中strtotime函数使用方法分享
2012/01/10 PHP
使用PHP实现蜘蛛访问日志统计
2013/07/05 PHP
PHP实现将优酷土豆腾讯视频html地址转换成flash swf地址的方法
2017/08/04 PHP
点击文章内容处弹出页面代码
2009/10/01 Javascript
javascript 事件查询综合 推荐收藏
2010/03/10 Javascript
JS拖动技术 关于setCapture使用
2010/12/09 Javascript
深入理解JavaScript系列(27):设计模式之建造者模式详解
2015/03/03 Javascript
深入浅析JavaScript中的constructor
2016/04/19 Javascript
jQuery配合coin-slider插件制作幻灯片效果的流程解析
2016/05/13 Javascript
网页挂马方式整理及详细介绍
2016/11/03 Javascript
基于jQuery实现简单人工智能聊天室
2017/02/10 Javascript
vue-axios使用详解
2017/05/10 Javascript
jQuery.Form实现Ajax上传文件同时设置headers的方法
2017/06/26 jQuery
js+html5实现半透明遮罩层弹框效果
2020/08/24 Javascript
Vue.js搭建移动端购物车界面
2020/06/28 Javascript
js最简单的双向绑定实例讲解
2018/01/02 Javascript
一次记住JavaScript的6个正则表达式方法
2018/02/22 Javascript
Vue2.0实现组件数据的双向绑定问题
2018/03/06 Javascript
Ajax请求时无法重定向的问题解决代码详解
2019/06/21 Javascript
vue项目强制清除页面缓存的例子
2019/11/06 Javascript
解决新建一个vue项目过程中遇到的问题
2020/10/22 Javascript
JavaScript实现点击切换功能
2021/01/27 Javascript
[52:52]完美世界DOTA2联赛PWL S3 LBZS vs access 第一场 12.10
2020/12/13 DOTA
PyTorch CNN实战之MNIST手写数字识别示例
2018/05/29 Python
Sanic框架蓝图用法实例分析
2018/07/17 Python
Python静态类型检查新工具之pyright 使用指南
2019/04/26 Python
Python中url标签使用知识点总结
2020/01/16 Python
通过Python实现一个简单的html页面
2020/05/16 Python
HTML5 WebGL 实现民航客机飞行监控系统
2019/07/25 HTML / CSS
班级课外活动总结
2014/07/09 职场文书
学校百日安全活动总结
2015/05/07 职场文书
安全教育日主题班会
2015/08/13 职场文书
如何使用python包中的sched事件调度器
2022/04/30 Python