前言
本文是、【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析) 第二篇。
这类文章一般都是给做react组件库深度玩家看的,原版的@popper-js 是用的flow类型系统,我这里用的ts。如果想了解主要逻辑,看上面【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析)这篇文章就够了。
废话不多说,我们继续写增强版的@popper-js 的逻辑。
渲染主逻辑分析
1、创建负责表示当前定位信息和更新定位信息的实例
首先调用 createPopper方法,传入3个参数
reference
:参考元素,即触发 popper 的元素。它可以是一个 DOM 元素或一个返回 DOM 元素的函数。Popper 将会根据reference
的位置计算 popper 的位置。popper
:popper 元素,即要进行定位的元素。它也可以是一个 DOM 元素或一个返回 DOM 元素的函数。Popper 将会根据reference
和popper
的关系来计算 popper 的位置。options
:外界传入的自定义参数,比如定位的位置,要top,还是bottom。
如下图:
2、初始化时调用了Instance对象的setOption方法,目的是合并options
例如我们默认弹出位置是reference的下方,但是外部可以自定位置,比如是reference的上方,所以需要合并options
3、setOption方法触发定位逻辑,主要是计算到底popper元素定位的坐标是多少
- 由于定位的时候,我们后续会滚动滚动条,定位元素最开始是在上方,由于滚动到最上面可能需要调整位置,如下图:
可以看到下图,由于浏览器滚动到上方,上方预留的空间不够popper显示到reference元素的上方,所以我们popper的位置变为了朝下。
所以我们就需要监听所有popper和reference元素的中父元素有滚动条的情况。
此时我们需要找到这些父元素。体现在代码:
javascript
state.scrollParents = {
reference: isElement(reference) ? listScrollParents(reference as Element) : [],
popper: listScrollParents(popper),
};
这里的关键函数是listScrollParents,它的逻辑简述为:
- 从当前节点开始,查看是否是具有滚动属性,判断方式:
javascript
export function isScrollParent(element: Element | HTMLElement): boolean {
const { overflow, overflowX, overflowY, display } = getComputedStyle(element);
return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['contents'].includes(display);
}
比如说,你的属性有overflow: auto, overflow: scroll等等,我认为你是可能具有滚动条的,但是为什么源码将overflow:hidden也算进去,就不知道为什么了,了解的同学可以留言区告知。
- 此时会检查reference和popper是否是html元素,如果不是就退出,并控制台警告,必须是html元素才行。 判断方式主要是查看是否有getBoundingRect方法:
javascript
export function areValidElements(...args: Array<any>): boolean {
return !args.some((element) => !(element && typeof element.getBoundingClientRect === 'function'));
}
- 接着计算reference元素的位置,也就是popper元素想定位,那么定位到reference元素的左上角的话,坐标是多少,对应代码如下:
javascript
state.rects = {
reference: getCompositeRect(reference, getOffsetParent(popper)),
popper: getLayoutRect(popper),
};
这里的计算方法【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析) 在这篇文章里有详细描述。
- 接着把计算出来了reference元素的位置经过中间件处理,比如说,我想把popper元素定位到reference元素的上方,那么首先我们说了,上面计算得到reference元素的左上角的坐标赋给popper,但是此时popper的左上角跟reference元素的左上角重合了
如果要放置到reference元素的上方,是不是还要把此时左上角的y坐标再减去popper元素的高度才对,然后x坐标需要:
arduino
reference.x + reference.width / 2 - popper.width / 2
至于为什么这样,你可以思考一下,这里你主要明白我的意思即可,要了解详细代码逻辑,具体代码地址在文章开始处。
- 我们这里的中间件主要有4个
popperOffsets中间件
这里主要是计算坐标,比如说刚才我们假设popper元素要定位到reference元素上方时,如何计算,那么所有位置的坐标换算,都在这个中间件处理。
这里位置示意图如下,共有12个位置:
computeStyles中间件
这个中间件有些逻辑有点绕,还是要说明一下
之前我们通过popperOffsets
中间件按道理来说已经计算完毕了,但还需要一些补充
- 假如此时我们popper相当于reference元素是在上方,如下图:
此时,我们加入popper的定位方式是:
javascript
position: 'absolute',
top: 100px
left: 100px
如果此时我将popper的内容(也就是This is a popup box),变为了两行,会发生什么情况呢?如下图:
因为高度变高了,换行的内容遮盖住了reference元素。如何解决呢,computeStyles组件里,我们发现是这种情况,把定位方式变一下:
之前是:
javascript
position: 'absolute',
top: 100px
left: 100px
我们改为:
javascript
position: 'absolute',
bottom: -600px
left: 100px
我们以bottom为标准,此时再改变高度,就变为:
是不是解决方式比较巧妙呢,但是最好的解决方式是什么,监听popper的元素如果有width和height的变化,重新计算定位坐标。
第二个重点是,computeStyles组件使用了css加速,利用transform将我们最终得到的x坐标和y坐标导出来,最终我们会在react层面把这个坐标赋值给popper元素的style属性。
offset中间件
比如,我们想要popper元素在定位后向上移动10px,或者向左移动10px,都可以将参数传入offset中间件,它就是来变换坐标的。
flip中间件
flip是目前遇到逻辑最复杂的中间件,它主要解决的问题是什么呢?
原理是,比如我们现在placement:bottom,表示定位到reference元素的下方,当我们向下滚动的时候,是不是这个定位的元素因为在下方,迟早会到视口的下面,如下图:
为了能看见tooltip,我们自动翻转到上方!
这就是flip的功能,至于如何实现,我们拿最简单的上面的案例做分析:
我们需要在滚动的时候,就检测上图的tooltip元素是否已经有部分超出浏览器视口了,只有检测到超出,我们是才能进行翻转。
检测思路:
- 首先滚动的地方可能不只是window窗口,还可能tooltip元素的父元素也能滚动,此时,tooltip元素的父元素的滚动也能让tooltip元素在滚动时隐藏,如下:
图中有两个滚动条,任意一个滚动到一定范围都能让蓝色的button组件隐藏。
所以我们要收集所有的滚动容器,他们中的所有上边框距离button元素最近的距离是多少,所有左边框距离button元素最近的距离是多少,下边和右边也是一样。
这样就计算出一个button元素活动的最小范围,在这个范围内随时有可能因为滚动而隐藏。
这个函数在@popper-js被抽离出叫做detectOverflow,最终返回一个对象:
javascript
{
top: popper元素距离最近的滚动容器上方的距离,正数表示还在可是区域内,
left: popper元素距离最近的滚动容器左边的距离,,正数表示还在可是区域内,
right: popper元素距离最近的滚动容器右方的距离,正数表示还在可是区域内,
bottom: popper元素距离最近的滚动容器下方的距离,,正数表示还在可是区域内,
}
这样,我们只要检查,比如popper元素此时应该是在上方,那么它的上边,左边和右边是否都是正数,只要有一个负数,说明此时popper元素已经隐藏部分区域了。
所以我们只要找到一个位置,它的3个方向都是正值,那么这个方向的popper元素就完全在可视区域内。
问题来了,如果所有的方向都不在可视区域内该怎么办呢?
那此时咋也不管了,原本是哪个方向就还是哪个方向,这在视觉上是完全没问题的。
本文完毕,如果本文你觉得还不错,欢迎给我的react 组件库项目satr哦,还在持续迭代中,我相信这个教程会成为全网最硬核的,能上生产环境react组件库项目!