详解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中使用mockjs代码实例
Nov 25 Vue.js
Vue项目如何引入bootstrap、elementUI、echarts
Nov 26 Vue.js
vue组件中节流函数的失效的原因和解决方法
Dec 02 Vue.js
如何正确解决VuePress本地访问出现资源报错404的问题
Dec 03 Vue.js
vue实现图片裁剪后上传
Dec 16 Vue.js
vue 在单页面应用里使用二级套嵌路由
Dec 19 Vue.js
vue中使用echarts的示例
Jan 03 Vue.js
vue登录页实现使用cookie记住7天密码功能的方法
Feb 18 Vue.js
vue项目配置 webpack-obfuscator 进行代码加密混淆的实现
Feb 26 Vue.js
vue前端工程的搭建
Mar 31 Vue.js
Vue.Draggable实现交换位置
Apr 07 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
php 将字符串按大写字母分隔成字符串数组
2010/04/30 PHP
php安全之直接用$获取值而不$_GET 字符转义
2012/06/03 PHP
php curl模拟post请求小实例
2013/11/13 PHP
php遍历文件夹下的所有文件和子文件夹示例
2014/03/20 PHP
通过php删除xml文档内容的方法
2015/01/23 PHP
PHP文件缓存smarty模板应用实例分析
2016/02/26 PHP
php中array_slice和array_splice函数解析
2016/10/18 PHP
php中Redis的应用--消息传递
2017/03/28 PHP
Yii2实现ActiveForm ajax提交
2017/05/26 PHP
php面向对象程序设计中self与static的区别分析
2019/05/21 PHP
JavaScript 指导方针
2007/04/05 Javascript
json 入门基础教程 推荐
2009/10/31 Javascript
jQuery新闻滚动插件 jquery.roller.js
2011/06/27 Javascript
JavaScript常用的返回,自动跳转,刷新,关闭语句汇总
2015/01/13 Javascript
javascript跨域方法、原理以及出现问题解决方法(详解)
2015/08/06 Javascript
JavaScript禁止复制与粘贴的实现代码
2016/05/16 Javascript
BootStrap实现手机端轮播图左右滑动事件
2016/10/13 Javascript
jQuery Datatable 多个查询条件自定义提交事件(推荐)
2017/08/24 jQuery
Node.js实现连接mysql数据库功能示例
2017/09/15 Javascript
vue-router实现tab标签页(单页面)详解
2017/10/17 Javascript
微信小程序实现身份证取景框拍摄
2020/09/09 Javascript
python实现的简单猜数字游戏
2015/04/04 Python
python代码实现ID3决策树算法
2017/12/20 Python
pycharm远程linux开发和调试代码的方法
2018/07/17 Python
python3的输入方式及多组输入方法
2018/10/17 Python
python代码实现TSNE降维数据可视化教程
2020/02/28 Python
python字符串常用方法及文件简单读写的操作方法
2020/03/04 Python
浅谈在JupyterNotebook下导入自己的模块的问题
2020/04/16 Python
Python日志器使用方法及原理解析
2020/09/27 Python
粉红色的鲸鱼:Vineyard Vines
2018/02/17 全球购物
纽约海:Sea New York
2018/11/04 全球购物
《望庐山瀑布》教学反思
2014/04/22 职场文书
助人为乐模范事迹材料
2014/06/02 职场文书
企业团队精神心得体会
2016/01/19 职场文书
2016大学生国家助学贷款承诺书
2016/03/25 职场文书
Django+Celery实现定时任务的示例
2021/06/23 Python