vue 弹窗_Vue3 & React Hooks 新UI组件原理:Modal 弹窗

在某个月黑风高的晚上...没剧刷的我无意想起以前处理的一些弹窗的坑。

d6b8a87aecd4d11d0ecf87dfceeecf8d.png然后又无意间刷到“Portal”,才知道Modal的实现还有如此妙的方式,顺而想着干脆把UI组件库的实现原理看完。

本文将讲述 Modal弹窗类的实现原理:

1. Modal弹窗的基本原理

我给弹窗类的定义是脱离固定的层级关系,不再受制于层叠上下文的组件。

常见的Modal模态框、Dialog对话框、Notification通知框等都是最最常用的交互方式。

3d98444429277c834210092a0cfea962.png在我们页面有时需要一些特定的弹窗时,通过改UI组件过于麻烦。

这时切图仔级别的会想:简单啊,创建一个 给绝对定位不就得了。

倘若只是当前路由页用,也还凑合。「可一旦涉及到了组件复用以及抽象为声明式,就会有很大的隐患」

  1. 若无封装,组件代码需要处处粘贴。
  2. 即使封装了,都是在每个路由页下创建,易造成样式污染。
  3. 类购物车的弹窗,又该如何处理数据及渲染?
  4. 再进一步想,万一组件库会作为绩效考核,拿到每个环境都长得不一样,咋整?
e260899a06914263a4d87ce2df956b00.png

1.1 Jquery时代的弹窗实现

初初入行时,去各种资源站,找JqueryUI组件,想必三四年经验的前端们都曾乐此不疲。

8ad0b99b482a77d8f9552b1d549f856b.png

这个时代(也就三四年前)的弹窗,因为没有React/Vue根节点的概念,普遍都是:

  1. 「直接操作真实 dom,使用熟知的 dom 操作方法将指令所在的元素 append 到另外一个 dom 节点上去。」 如:document.body.appendChild
  2. 再通过overflow: hiddendisplay:none(或调整z-index)来隐藏。

这种操作真实dom的代价,在大型项目中不停触发重绘/回流,是很糟糕的,且内部数据/样式不易更改。像以下这种情况就容易出现:

  1. 原本图片固定在区域内。b4f76179e37a18c15ca6a73bf1367900.png
  2. 小弹窗展示后,溢出了。6616f428b31a691c4978f7fd27d9f209.png随着React / Vue先进库的发展,也陆续有了多种方案选择。。。

1.2 React / Vue早期实现。

其实React / Vue早期的实现和Jquery时代的并无二异:「依赖于父节点数据,在当前组件内挂载弹窗。」

Vue的情况稍好,有自定义指令这条路走。

以下引自:《Vue 中的 Portal 技术》

vue-dom-portal为例,代码非常简单无非就是将当前的 dom 移动到指定地方:

23b5f6657964477d2f8c2ab404eb7be8.png

可以看到在 inserted 的时候就拿到实例的 el(真实 dom),然后进行替换操作,在 componentUpdated 的时候再次根据指令的值去操作 dom。

为了能够在不同声明周期函数中使用缓存的一些数据,这里在 inserted 的时候就把当前节点的父节点和替换成的 dom 节点(一个注释节点),以及节点是否移出去的状态都记录在外部的一个 map 中,这样可以在其他的声明周期函数中使用,可以避免重复计算。

但是React / Vue的实现都有类似的通病:

  1. 生命周期的执行会很混乱。
  2. 需要通过reduxprops管理数据,可这对于一个UI组件来说过于臃肿了。

React官方也意识到构建脱离于父组件的组件挺麻烦的,于是在v16版本推了一个叫“Portal ”的功能。而Vue3也是借鉴并吸纳了优秀插件,将Portal作为内置组件了。

1.3 传送门Portal方案

c43f80aad0b8a83bff8f34e14837380d.pngReact / Vue的第二套方案都是基于操作虚拟dom

「定义一套组件,将组件内的 vnode/ReactDOM 转移到另外一个组件中去,然后各自渲染。」

2. ReactPortal

React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。

ReactDOM.createPortal(child, container)
  1. 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或碎片。
  2. 第二个参数(container)则是一个 DOM 元素。

v16中,使用Portal创建Dialog组件简单多了,不需要牵扯到componentDidMountcomponentDidUpdate,也不用调用API清理Portal,关键代码在 render 中,像下面这样就行:

6311b7574865dd9a4ae3eb86c34fd99a.png

当然,我们作为一个React Hooks选手,不骚一下咋行。

2.1 热门组件库Ant Design中的实现

原本是想从Ant Design库中一窥究竟,却发现事情并不简单。。

a64f7cdd98128e1fb6173e5ba92fec94.png

前后寻址了三个库/地方,才发现实现的关键:

  1. import Dialog from 'rc-dialog';
  2. import Portal from 'rc-util/lib/PortalWrapper';
  3. import Portal from './Portal';

具体实现也算如我所料:

6b12ef20e86e072f10f8afa7fa5d9eaa.png

7e3326aae76fb1becc5ec477d21aa98f.png

render里用了ReactDOM.createPortal`

**这也是为什么多数Modal组件不会提供篡改整体样式的API,只能通过全局重置样式。`

2.1 React Hooks版弹窗:useModal

413301b451a000d93d3f1fb3b52d63d9.png
步骤一:创建一个Modal组件

211ff427ebeb783b6ea7cb46f6086b75.png

步骤二:自定义useModal

2287c0dde80f0c01893aeb4dc3e1cacc.png

很好理解,不懂的建议转行写Vue

c8c797958163f576c3f9256d63db2e66.png
步骤三:使用它

4a453e1e8ab7fd0c91d611b7a636415d.png

3.  Vue3Portal

Vue虽说是借鉴,但使用方式可容易多了。

758434de5bea5c726b2b0e866e63d5b0.png

在上面的示例中,该组件将在id=portal-target的容器中渲染,即使它位于OtherComponent组件内。

这,这...这也太香了吧。进一步的用法如下:

77ee9236201cec65350cd0e8d2243783.png

b0dceac3e5acf7ed85933a860a97a84b.png

然后我再去找了下Vue3的源码实现:在packages/runtime-core/src/components/Portal.ts目录中:

87bcedb1da766dc58a732d09485cb257.png

重要的解释,都在上述注释中了,临时看的,说得不对的谢谢指正。

其中:createCommentVueDOM.createComment的进一步封装。

结语&参考

这篇算是自己半夜无聊折腾出来的,原定计划是一篇写三种组件,但弹窗类的实现比较有意思。

b354ee8cc116500970c2a55990075145.png

这个系列我会看着写,不出意外下一篇就是讲Steps步骤条和Transfer穿梭框的实现(当然,太难了就忽悠一下,嘿嘿。)

「参考文章:」

  1. 《Building a simple and reusable modal with React hooks and portals》
  2. 《Vue 中的 Portal 技术 》
  3. 《Portal – a new feature in Vue 3》
  4. 《React Portal 的前世今生 》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值