DOM – ResizeObserver

介绍

想监听一个 element 的 size changes 就可以使用 ResizeObserver 了.

在看这一篇之前, 建议先看看 DOM & BOM – IntersectionObserver, 它们的模式很像, 一起了解会比较容易.

 

效果

参考:

MDN – ResizeObserver

Youtube – Learn Resize Observer In 5 Minutes

 

场景

  <body>
    <button class="button">resize</button>
    <div class="box">resize me</div>
  </body>

CSS Style

.box {
  margin-top: 3rem;
  width: 100px;
  height: 100px;
  background-color: pink;
  border: 2px solid red;
  padding: 1rem;
}

 

new ResizeObserver()

const rs = new ResizeObserver((entries) => {
  console.log("entry", entries[0]);
});

rs.observe(document.querySelector(".box"));

document.querySelector(".button").addEventListener("click", () => {
  document.querySelector(".box").style.width = "400px";
});

调用, observe, unobserve 方式和 IntersectionObserver 是一样的.

new 实例 > observe > 当观察的元素 resize 就会触发回调. 不想观察了就 unobserve。

 

Callback Info

绿点是常用到的, 其它的就不介绍了

borderBoxSize, 它是 array 哦, 但我不知道什么时候会超过 1 个啦.

blockSize 就是 height, inlineSize 就是 width. 这个是 Logical Properties 的写法.

ContentBoxSize, 顾名思义它就是依据 box-sizing: content-box 的计算方式 (width 不包含 padding 和 border).

contentRect: 它的 width 和 height 是扣除了 padding, border 的, 至于 rect, 它并不是 boundingClientRect 哦, 具体是什么坐标我也没去研究, 以后有需求在来补上呗.

 

触发时机

ResizeObserver 会在 ui render 后触发,据说是在 layout 之后 paint 之前。

window.setTimeout(() => {
  const h1 = document.querySelector('h1')!;
  h1.classList.add('showing');

  const io = new ResizeObserver(() => {
    console.log('ResizeObserver', performance.now());           // 2, 1088.0999999940395
    h1.classList.add('showed');
  });
  io.observe(h1);

  requestAnimationFrame(() => {
    console.log('first requestAnimationFrame', performance.now());    // 1, 1083.7999999970198
    requestAnimationFrame(() => {
      console.log('second requestAnimationFrame', performance.now()); // 3, 1094.0999999940395
    });
  });
}, 1000);

第一次 requestAnimationFrame 触发在 ui render 之前 

第二次 requestAnimationFrame 触发在 ui render 之后

ResizeObserver 在第一次 rAF 和第二次 rAF 中间触发。(注:ResizeObserver 和 IntersectionObserver 一样,observe 后会触发第一次)

另外一点,改了又改回去是不会触发 ResizeObserver 的

const box = document.querySelector(".box");
const ro = new ResizeObserver(() => console.log("resize"));
ro.observe(box);

window.setTimeout(() => {
   // 修改 dimension
   box.style.width = '100px';
   box.style.height = '100px';

   // 读取 dimension,这里会导致游览器提前 reflow
   console.log(box.offsetWidth); // 100

   // 修改回去
   box.style.width = 'auto';
   box.style.height = 'auto';
}, 2000);

ResizeObserver 不会触发,即便中间有一个 reflow 也不会。

日常初始化 best practices

 

resize 通常是用来做同步 size 的。

比如 A 变高以后,B 也要变高,或变矮。(总之就是一些布局上的同步逻辑)

假设我们的需求是同步 A 和 B 的高

初始化时,A 高 100px,我们监听 A resize

A 第一次触发 resize 是在 ui render 之后

如果我们等到此时才去同步 B 的高

那用户就会看见一开始 B 是 0px 然后去到 100px。

这样跳一下体验不好。

不要等到第一次触发才做初始化同步,慢一拍了。

我们应该在监听 A 的同时,直接执行第一轮的同步。

等到 resize 第一次触发,B 已经是 100px 了,

此时有两个选择:

  1.  再同步一次。

    其实也不要紧,因为从 100px update 到 100px 不会导致浏览器 reflow(因为高度实际上没有变化)

  2. skip 掉第一次的 resize 触发(既然是 "resize",那应该是变化才触发,而不是一定会触发第一次)

    注意:前提是我们能保证从监听的那一刻,到第一次触发,这段期间没有任何 resize 操作,否则就坏了。

Multiple ResizeObserver 之前的触发时机

有两个 ResizeObserver 同时 observe 一个 element

const div = document.createElement('div');
document.body.appendChild(div);
const ro1 = new ResizeObserver(() => {
  log('ro1');
  queueMicrotask(() => log('mic1'));
  window.requestAnimationFrame(() => log('raf1'));
});
const ro2 = new ResizeObserver(() => log('ro2'));
ro1.observe(div);
ro2.observe(div);
div.style.height = '200px';

// 触发顺序:ro1...mic1...ro2...raf1

当这个 element 变更时

  1. 哪一个 ResizeObserver 先触发?

    答案是先创建实例的那一个先触发。

    不是看谁先 observe 哦,是看谁先实例化出 ResizeObserver 对象。

  2. 它们触发之间的间隔又是多久?

    视乎有一个 microtask 间隔,这就类似于 multi listen click event

    我们在 ro1 callback 里 queueMicrotask,它会比 ro2 callback 还要早被执行。

 

Uncaught ResizeObserver loop completed with undelivered notifications

有一个 container 和 change size button

<div class="container">
  <button>change size</button>
</div>

点击 button 后,container 会增加 100px

const button = document.querySelector('button');

/** @type {HTMLElement} */
const container = document.querySelector('.container');

button.addEventListener('click', () => {
  container.style.width = `${container.offsetWidth + 100}px`;
});

与此同时,我们监听 container resize,每当 resize 后,我们又加 100px,一直到 1000px 才停。

const ro = new ResizeObserver(() => {
  if(container.offsetWidth <= 1000) {
    container.style.width = `${container.offsetWidth + 100}px`;
  }
});
ro.observe(container);

执行上述代码,会得到一个游览器 error。

它的意思是说,我们在 ResizeObserver callback 里又做了 resize,从而导致了 resize callback 又一次被执行。

虽然它可以执行,效果也对 (注:可能某些情况下会出错哦),但就是会报错。

解决方法是给它一个 delay

const ro = new ResizeObserver(() => {
  if(container.offsetWidth <= 1000) {
    window.requestAnimationFrame(() => container.style.width = `${container.offsetWidth + 100}px`) ;
  }
});
ro.observe(container);

这样就不会报错了。

 

当遇上 display: none, visibility: hidden 和 opacity: 0

hidden 和 opacity 只是看不见,但 element 任然有 size,因此没有任何特别。

display:none 会导致 element width height 变 0,display: block 又恢复原本的 width height,这种切换算是 resize,因此 ResizeObserver 也能监听的到。

 

posted @ 2022-03-11 19:38  兴杰  阅读(311)  评论(0)    收藏  举报