详解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自定义插件封装,实现简易的elementUi的Message和MessageBox的示例
Nov 20 Vue.js
Vue3+elementui plus创建项目的方法
Dec 01 Vue.js
Vue 组件注册全解析
Dec 17 Vue.js
vue+elementUI动态增加表单项并添加验证的代码详解
Dec 17 Vue.js
vue 实现图片懒加载功能
Dec 31 Vue.js
Vue中ref和$refs的介绍以及使用方法示例
Jan 11 Vue.js
Vue+Element UI实现概要小弹窗的全过程
May 30 Vue.js
vue3中provide && inject的使用
Jul 01 Vue.js
vue实现移动端div拖动效果
Mar 03 Vue.js
vue中div禁止点击事件的实现
Apr 02 Vue.js
vue 数字翻牌器动态加载数据
Apr 20 Vue.js
vue @ ~ 相对路径 路径别名设置方式
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+mysql实现数据库随机重排实例
2014/10/17 PHP
SSO单点登录的PHP实现方法(Laravel框架)
2016/03/23 PHP
PHP设计模式之装饰器模式实例详解
2018/02/07 PHP
php use和include区别总结
2019/10/13 PHP
laravel高级的Join语法详解以及使用Join多个条件
2019/10/16 PHP
js 实现无干扰阴影效果 简单好用(附文件下载)
2009/12/27 Javascript
新老版本juqery获取radio对象的方法
2010/03/01 Javascript
利用jQuery接受和处理xml数据的代码(.net)
2011/03/28 Javascript
关于scrollLeft,scrollTop的浏览器兼容性测试
2013/03/19 Javascript
jquery+json实现数据列表分页示例代码
2013/11/15 Javascript
js实现二级菜单渐隐显示
2015/11/03 Javascript
jquery ui dialog替代confirm实例分析
2016/01/25 Javascript
JavaScript:Date类型全面解析
2016/05/19 Javascript
基于JavaScript代码实现自动生成表格
2016/06/15 Javascript
AngularJs  Creating Services详解及示例代码
2016/09/02 Javascript
js print打印网页指定区域内容的简单实例
2016/11/01 Javascript
jQuery的Ajax接收java返回数据方法
2018/08/11 jQuery
js中this的指向问题归纳总结
2018/11/28 Javascript
[46:25]DOTA2上海特级锦标赛主赛事日 - 4 败者组第五轮 MVP.Phx VS EG第二局
2016/03/05 DOTA
[46:37]LGD vs TNC 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/16 DOTA
python3 破解 geetest(极验)的滑块验证码功能
2018/02/24 Python
解决Python 中英文混输格式对齐的问题
2018/07/16 Python
Python3通过chmod修改目录或文件权限的方法示例
2020/06/08 Python
django filter过滤器实现显示某个类型指定字段不同值方式
2020/07/16 Python
python des,aes,rsa加解密的实现
2021/01/16 Python
美国最大的船只买卖在线市场:Boat Trader
2018/08/04 全球购物
单位办理社保介绍信
2014/01/10 职场文书
九年级数学教学反思
2014/02/02 职场文书
公司聘任书模板
2014/03/29 职场文书
舞蹈专业求职信
2014/06/13 职场文书
毕业生求职信范文
2014/06/29 职场文书
2015年党小组工作总结
2015/05/26 职场文书
担保书怎么写 ?
2019/04/22 职场文书
2019幼儿教师求职信(3篇)
2019/09/20 职场文书
Html分层的box-shadow效果的示例代码
2021/03/30 HTML / CSS
mysql的MVCC多版本并发控制的实现
2021/04/14 MySQL