编程语言/getBoundingClientRect

来自康健生活
跳到导航 跳到搜索

引言

getBoundingClientRect

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
Warning: 很绕口的地方需要注意,有些属性是基于视窗,并不是基于网页整体大小

DOMRect

这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合。

getBoundingClientRect

bottom

是距离视窗(浏览器标签窗口,并不是页面最顶部)的距离

问题小记

现在有一个需求,有三个 DOM,分别是 body、点击按钮、弹出框。需要将弹出框的定位定在按钮的旁边,同时根据 body 限定右边界和下边界,当弹出框超出边界时,自动左移或上移。

GetBoundingClientRect 1

在代码结构中,需要将影响 getBoundingClientRect 获取页面实际渲染尺寸的代码,放在计算相关代码上面。

...
export default function({
  ...
  getPopupContainer = () => document.body,
  isAutoAdaptPosition = false,
  prefix = [0, 0]
}) {
  ...
  const root = useRef(document.createElement('div'));
  ...
  /**
   * 网页可见区域高 container.getBoundingClientRect().height
   * container 是 document.body 也就是整个画布
   * root 是弹出框
   * triggerRef 是点击按钮
   * 如果 root 的设计到布局的样式放到底部,会导致 root 第一次显示的宽度是 document.body 的宽度
   */
  const scrollingElement = (document.scrollingElement||document.body);
  const calc = () => {

    const container = getPopupContainer();

    // kangkk: 涉及到影响 ref 长宽高及其坐标属性的 css 样式更改,应该放到最前面进行。
    // 否则 getBoundingClientRect 获取不到准确值
    root.current.style.position = container === document.body ? 'fixed' : 'absolute';
    root.current.className = 'dropdown-trigger__box--ref'
    root.current.style.zIndex = 5000;

    const {x, y, height, bottom: triggerRefBottom, right: triggerRefRight} = triggerRef.current.getBoundingClientRect();
    const {x: cx, y: cy, bottom: containerBottom, right: containerRight} = container.getBoundingClientRect();

    let offsetTop = y + height - cy;
    let offsetLeft = x - cx;
    let triggerRefHeight = triggerRefBottom - y;
    let triggerRefWidth = triggerRefRight - x;
    let rootRefWidth = root.current.getBoundingClientRect().width;
    let rootRefHeight = root.current.getBoundingClientRect().height;

    if(container === document.body) {
      const scrollTop = scrollingElement.scrollTop;
      const scrollLeft = scrollingElement.scrollLeft;
      offsetTop -= scrollTop;
      offsetLeft -= scrollLeft;
    }

    //  kangkk: 自动适应位置
    if(isAutoAdaptPosition){
      offsetTop = (((triggerRefBottom - triggerRefHeight/2) + rootRefHeight) > containerBottom)
                          ?((triggerRefBottom - triggerRefHeight/2) - rootRefHeight - prefix[1]) 
                          : (offsetTop + prefix[1])
      offsetLeft = (((triggerRefRight - triggerRefWidth/2) + rootRefWidth) > containerRight)
                          ?((triggerRefRight - triggerRefWidth/2) - rootRefWidth - prefix[0]) 
                          : (offsetLeft + triggerRefWidth/2 + prefix[0])
    }
    // 这行代码放在此处,会丢失 margin-top
    // root.current.style.position = container === document.body ? 'fixed' : 'absolute';

    console.log('rootRefWidth',rootRefWidth)
    console.log('rootRefHeight',rootRefHeight)

    root.current.style.top = offsetTop + 'px';
    root.current.style.left = offsetLeft + 'px';
  }
  ...
  useEffect(() => {
    if (visible) {
      getPopupContainer().appendChild(root.current);
      ...
    } else {
      try { getPopupContainer().removeChild(root.current) } catch(e) {}
      ...
    }
  }, [visible])

  return (
    <div className={cls('dropdown', className)} ref={dropdownRef}>
      {ReactDOM.createPortal((
        ...
      ), root.current)}
    </div>
  )
}

不然会出现 getBoundingClientRect 计算误差,比如如下:

GetBoundingClientRect 2

在此示例中 getBoundingClientRect 计算高度缺少了一个 margin-top,同时宽度变成了 body 的宽度。

问题回顾

我先将 root.current.style.position = container === document.body ? 'fixed' : 'absolute'; 放在上述代码的第 56 行。点击页面中的“启动页”选项卡,其结果(第一次点击)如下:

GetBoundingClientRect问题分析 1

盒模型为。

GetBoundingClientRect问题分析 2

文档中 dropdown-trigger__box--ref 已经固定布局,但是其内部子元素却缺失了 margin-top (当 console.log 时,页面结构已经触发回流,其子元素存在 margin-top)

GetBoundingClientRect问题分析 3

console 结果:

rootRefWidth 1092.800048828125
rootRefHeight 244

第二次点击

console 结果

rootRefWidth 147
rootRefHeight 248

为什么会是 244

margin 合并

回流

如果 dom 插入 body 中,正常的流程是计算属性布局,当满足 margin 合并条件时,触发 margin 合并,然后渲染完成。但是如果接下来的 js 代码修改了某个元素的一些属性(eq:重新 position),假设使某元素固定定位,那么元素将脱离文档流,触发回流,重新计算属性布局,此时重新渲染出来的元素必然是拥有完整的 margin 的(那么丢失的 margin-top 为什么没有重新渲染出来)

  1. 动态创建元素
  2. 插入 body,发生 margin 塌陷
  3. 修改元素定位触发 BFC(eq: 固定定位)
  4. 回流
  5. 重新计算布局,元素具有完整的 margin

也就是说元素已经被固定定位的情况下,必然是已经发生过回流,布局重新计算的情况下,为什么会丢失 margin-top。

这里我们做一个假设。


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=, initial-scale=">
	<meta http-equiv="X-UA-Compatible" content="">
	<title></title>
</head>
<body>
	<div style="height: 60px;margin-bottom: 40px; background: red;"></div>
</body>
</html>
.test{
    margin-top: 40px;
    width: 100%;
    height: 40px;
    background: green;
}
var dom = document.createElement('div');
setInterval(()=>{
    dom.style.position = 'fixed'
    dom.className = 'test';
    document.body.appendChild(dom)

},3000)


固定定位触发回流时重新计算margin


我们可以看到当 div 插入文档中时,会发生上下外边距合并(塌陷)

为什么回流之后还是 244

获取子元素的信息是在改变其定位之前。

先插入文档,子元素与父元素的子元素发生 margin 合并

为什么后面变成了 248

getBoundingClientRect 计算不包括 margin

其他

以上我们思考为什么会丢失 margin-top,接下来思考如果已经开始触发 BFC

整理逻辑中 待续

参考