【全网最硬核的react组件库教程】手写增强版 @popper-js (逻辑代码完整版)

前言

本文是、【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析) 第二篇。

这类文章一般都是给做react组件库深度玩家看的,原版的@popper-js 是用的flow类型系统,我这里用的ts。如果想了解主要逻辑,看上面【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析)这篇文章就够了。

废话不多说,我们继续写增强版的@popper-js 的逻辑。

渲染主逻辑分析

1、创建负责表示当前定位信息和更新定位信息的实例

首先调用 createPopper方法,传入3个参数

  1. reference:参考元素,即触发 popper 的元素。它可以是一个 DOM 元素或一个返回 DOM 元素的函数。Popper 将会根据 reference 的位置计算 popper 的位置。
  2. popper:popper 元素,即要进行定位的元素。它也可以是一个 DOM 元素或一个返回 DOM 元素的函数。Popper 将会根据 reference 和 popper 的关系来计算 popper 的位置。
  3. options:外界传入的自定义参数,比如定位的位置,要top,还是bottom。

如下图:

image.png

2、初始化时调用了Instance对象的setOption方法,目的是合并options

例如我们默认弹出位置是reference的下方,但是外部可以自定位置,比如是reference的上方,所以需要合并options

3、setOption方法触发定位逻辑,主要是计算到底popper元素定位的坐标是多少

  • 由于定位的时候,我们后续会滚动滚动条,定位元素最开始是在上方,由于滚动到最上面可能需要调整位置,如下图: image.png

可以看到下图,由于浏览器滚动到上方,上方预留的空间不够popper显示到reference元素的上方,所以我们popper的位置变为了朝下。

image.png

所以我们就需要监听所有popper和reference元素的中父元素有滚动条的情况。

此时我们需要找到这些父元素。体现在代码:

  state.scrollParents = {
        reference: isElement(reference) ? listScrollParents(reference as Element) : [],
        popper: listScrollParents(popper),
      };

这里的关键函数是listScrollParents,它的逻辑简述为:

  • 从当前节点开始,查看是否是具有滚动属性,判断方式:
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方法:
export function areValidElements(...args: Array<any>): boolean {
  return !args.some((element) => !(element && typeof element.getBoundingClientRect === 'function'));
}

  • 接着计算reference元素的位置,也就是popper元素想定位,那么定位到reference元素的左上角的话,坐标是多少,对应代码如下:
      state.rects = {
        reference: getCompositeRect(reference, getOffsetParent(popper)),
        popper: getLayoutRect(popper),
      };

这里的计算方法【目前最好的react组件库教程】手写增强版 @popper-js (主体逻辑分析) 在这篇文章里有详细描述。

  • 接着把计算出来了reference元素的位置经过中间件处理,比如说,我想把popper元素定位到reference元素的上方,那么首先我们说了,上面计算得到reference元素的左上角的坐标赋给popper,但是此时popper的左上角跟reference元素的左上角重合了

如果要放置到reference元素的上方,是不是还要把此时左上角的y坐标再减去popper元素的高度才对,然后x坐标需要:

reference.x + reference.width / 2 - popper.width / 2

至于为什么这样,你可以思考一下,这里你主要明白我的意思即可,要了解详细代码逻辑,具体代码地址在文章开始处。

  • 我们这里的中间件主要有4个

popperOffsets中间件

这里主要是计算坐标,比如说刚才我们假设popper元素要定位到reference元素上方时,如何计算,那么所有位置的坐标换算,都在这个中间件处理。

这里位置示意图如下,共有12个位置:

image.png

computeStyles中间件

这个中间件有些逻辑有点绕,还是要说明一下

之前我们通过popperOffsets中间件按道理来说已经计算完毕了,但还需要一些补充

  • 假如此时我们popper相当于reference元素是在上方,如下图:

image.png

此时,我们加入popper的定位方式是:

position: 'absolute',
top: 100px
left: 100px

如果此时我将popper的内容(也就是This is a popup box),变为了两行,会发生什么情况呢?如下图:

image.png

因为高度变高了,换行的内容遮盖住了reference元素。如何解决呢,computeStyles组件里,我们发现是这种情况,把定位方式变一下:

之前是:

position: 'absolute',
top: 100px
left: 100px

我们改为:

position: 'absolute',
bottom: -600px
left: 100px

我们以bottom为标准,此时再改变高度,就变为:

image.png

是不是解决方式比较巧妙呢,但是最好的解决方式是什么,监听popper的元素如果有width和height的变化,重新计算定位坐标。

第二个重点是,computeStyles组件使用了css加速,利用transform将我们最终得到的x坐标和y坐标导出来,最终我们会在react层面把这个坐标赋值给popper元素的style属性。

offset中间件

比如,我们想要popper元素在定位后向上移动10px,或者向左移动10px,都可以将参数传入offset中间件,它就是来变换坐标的。

flip中间件

flip是目前遇到逻辑最复杂的中间件,它主要解决的问题是什么呢?

原理是,比如我们现在placement:bottom,表示定位到reference元素的下方,当我们向下滚动的时候,是不是这个定位的元素因为在下方,迟早会到视口的下面,如下图:

image.png

为了能看见tooltip,我们自动翻转到上方!

image.png

这就是flip的功能,至于如何实现,我们拿最简单的上面的案例做分析:

我们需要在滚动的时候,就检测上图的tooltip元素是否已经有部分超出浏览器视口了,只有检测到超出,我们是才能进行翻转。

检测思路:

  • 首先滚动的地方可能不只是window窗口,还可能tooltip元素的父元素也能滚动,此时,tooltip元素的父元素的滚动也能让tooltip元素在滚动时隐藏,如下:

image.png

图中有两个滚动条,任意一个滚动到一定范围都能让蓝色的button组件隐藏。

所以我们要收集所有的滚动容器,他们中的所有上边框距离button元素最近的距离是多少,所有左边框距离button元素最近的距离是多少,下边和右边也是一样。

这样就计算出一个button元素活动的最小范围,在这个范围内随时有可能因为滚动而隐藏。

这个函数在@popper-js被抽离出叫做detectOverflow,最终返回一个对象:

{
    top: popper元素距离最近的滚动容器上方的距离,正数表示还在可是区域内,
    left: popper元素距离最近的滚动容器左边的距离,,正数表示还在可是区域内,
    right: popper元素距离最近的滚动容器右方的距离,正数表示还在可是区域内,
    bottom: popper元素距离最近的滚动容器下方的距离,,正数表示还在可是区域内,
}

这样,我们只要检查,比如popper元素此时应该是在上方,那么它的上边,左边和右边是否都是正数,只要有一个负数,说明此时popper元素已经隐藏部分区域了。

所以我们只要找到一个位置,它的3个方向都是正值,那么这个方向的popper元素就完全在可视区域内。

问题来了,如果所有的方向都不在可视区域内该怎么办呢?

那此时咋也不管了,原本是哪个方向就还是哪个方向,这在视觉上是完全没问题的。

本文完毕,如果本文你觉得还不错,欢迎给我的react 组件库项目satr哦,还在持续迭代中,我相信这个教程会成为全网最硬核的,能上生产环境react组件库项目!

猜你喜欢

转载自juejin.im/post/7264921613544390708