前言
在某个月黑风高的晚上...没剧刷的我无意想起以前处理的一些弹窗的坑。
然后又无意间刷到“Portal
”,才知道Modal
的实现还有如此妙的方式,顺而想着干脆把UI
组件库的实现原理看完。
本文将讲述 Modal
弹窗类的实现原理:
1. Modal
弹窗的基本原理
我给弹窗类的定义是脱离固定的层级关系,不再受制于层叠上下文的组件。
常见的Modal
模态框、Dialog
对话框、Notification
通知框等都是最最常用的交互方式。
在我们页面有时需要一些特定的弹窗时,通过改UI
组件过于麻烦。
这时切图仔级别的会想:简单啊,创建一个<div/>
给绝对定位不就得了。
倘若只是当前路由页用,也还凑合。「可一旦涉及到了组件复用以及抽象为声明式,就会有很大的隐患」:
- 若无封装,组件代码需要处处粘贴。
- 即使封装了,都是在每个路由页下创建
<div/>
,易造成样式污染。 - 类购物车的弹窗,又该如何处理数据及渲染?
- 再进一步想,万一组件库会作为绩效考核,拿到每个环境都长得不一样,咋整?
1.1 Jquery
时代的弹窗实现
初初入行时,去各种资源站,找Jquery
的UI
组件,想必三四年经验的前端们都曾乐此不疲。
这个时代(也就三四年前)的弹窗,因为没有React
/Vue
根节点的概念,普遍都是:
- 「直接操作真实 dom,使用熟知的 dom 操作方法将指令所在的元素 append 到另外一个 dom 节点上去。」 如:
document.body.appendChild
。 - 再通过
overflow: hidden
或display:none
(或调整z-index
)来隐藏。
这种操作真实dom
的代价,在大型项目中不停触发重绘/回流,是很糟糕的,且内部数据/样式不易更改。像以下这种情况就容易出现:
- 原本图片固定在区域内。
- 小弹窗展示后,溢出了。
随着React / Vue
先进库的发展,也陆续有了多种方案选择。。。
1.2 React / Vue
早期实现。
其实React / Vue
早期的实现和Jquery
时代的并无二异:「依赖于父节点数据,在当前组件内挂载弹窗。」
Vue
的情况稍好,有自定义指令这条路走。
❝以下引自:《Vue 中的 Portal 技术》 ❞
以vue-dom-portal
为例,代码非常简单无非就是将当前的 dom
移动到指定地方:
可以看到在 inserted
的时候就拿到实例的 el(真实 dom),然后进行替换操作,在 componentUpdated
的时候再次根据指令的值去操作 dom。
为了能够在不同声明周期函数中使用缓存的一些数据,这里在 inserted
的时候就把当前节点的父节点和替换成的 dom
节点(一个注释节点),以及节点是否移出去的状态都记录在外部的一个 map
中,这样可以在其他的声明周期函数中使用,可以避免重复计算。
但是React / Vue
的实现都有类似的通病:
- 生命周期的执行会很混乱。
- 需要通过
redux
或props
管理数据,可这对于一个UI
组件来说过于臃肿了。
React
官方也意识到构建脱离于父组件的组件挺麻烦的,于是在v16
版本推了一个叫“Portal
”的功能。而Vue3
也是借鉴并吸纳了优秀插件,将Portal
作为内置组件了。
1.3 传送门Portal
方案
React / Vue
的第二套方案都是基于操作虚拟dom
:
「定义一套组件,将组件内的 vnode/ReactDOM
转移到另外一个组件中去,然后各自渲染。」
2. React
的Portal
React Portal
之所以叫Portal
,因为做的就是和“传送门”一样的事情:render
到一个组件里面去,实际改变的是网页上另一处的DOM
结构。
ReactDOM.createPortal(child, container)
- 第一个参数(
child
)是任何可渲染的React
子元素,例如一个元素,字符串或碎片。 - 第二个参数(
container
)则是一个DOM
元素。
在v16
中,使用Portal
创建Dialog
组件简单多了,不需要牵扯到componentDidMount
、componentDidUpdate
,也不用调用API
清理Portal
,关键代码在 render 中,像下面这样就行:
当然,我们作为一个React Hooks
选手,不骚一下咋行。
2.1 热门组件库Ant Design
中的实现
原本是想从Ant Design
库中一窥究竟,却发现事情并不简单。。
前后寻址了三个库/地方,才发现实现的关键:
import Dialog from 'rc-dialog';
import Portal from 'rc-util/lib/PortalWrapper';
import Portal from './Portal';
具体实现也算如我所料:
render里用了
ReactDOM.createPortal`
**这也是为什么多数Modal
组件不会提供篡改整体样式的API
,只能通过全局重置样式。`
2.1 React Hooks
版弹窗:useModal
步骤一:创建一个Modal
组件
步骤二:自定义useModal
很好理解,不懂的建议转行写Vue
。
步骤三:使用它
3. Vue3
的Portal
Vue
虽说是借鉴,但使用方式可容易多了。
在上面的示例中,该<Modal />
组件将在id=portal-target
的容器中渲染,即使它位于OtherComponent
组件内。
这,这...这也太香了吧。进一步的用法如下:
然后我再去找了下Vue3
的源码实现:在packages/runtime-core/src/components/Portal.ts
目录中:
重要的解释,都在上述注释中了,临时看的,说得不对的谢谢指正。
其中:createComment
是Vue
对DOM.createComment
的进一步封装。
结语&参考
这篇算是自己半夜无聊折腾出来的,原定计划是一篇写三种组件,但弹窗类的实现比较有意思。
这个系列我会看着写,不出意外下一篇就是讲Steps
步骤条和Transfer
穿梭框的实现(当然,太难了就忽悠一下,嘿嘿。)
「参考文章:」
- 《Building a simple and reusable modal with React hooks and portals》
- 《Vue 中的 Portal 技术 》
- 《Portal – a new feature in Vue 3》
- 《React Portal 的前世今生 》