JS `ResizeObserver`:监听元素内容区域尺寸变化与响应式布局

各位观众老爷,早上好/中午好/晚上好!欢迎来到今天的 “JS ResizeObserver:监听元素内容区域尺寸变化与响应式布局” 讲座。今天咱们就来聊聊这个在前端开发中相当实用,但又经常被忽略的小工具——ResizeObserver

开场白:为什么需要 ResizeObserver?

想象一下,你在开发一个复杂的Web应用,页面布局需要根据不同设备的屏幕尺寸,甚至元素的自身尺寸进行动态调整。你可能会用到window.addEventListener('resize', ...)来监听窗口的resize事件,但这种方式有几个问题:

  1. 全局监听,性能损耗: 每次窗口resize,都会触发回调函数,即便你只想监听某个特定元素的尺寸变化。这无疑是对性能的浪费。
  2. 元素尺寸变化检测不精准: 有些元素的尺寸变化并非由窗口resize引起,比如CSS动画、JavaScript动态修改、子元素内容撑开等等。window.resize监听不到这些变化。
  3. 回调函数执行频率过高: 窗口resize事件触发非常频繁,导致回调函数频繁执行,可能会引发性能问题,甚至卡顿。

这时候,ResizeObserver就派上用场了。它能够监听特定元素的内容区域尺寸变化,并在变化发生时执行回调函数,而且还针对性能进行了优化。简单来说,它更精准、更高效。

ResizeObserver 是什么?

ResizeObserver 是一个现代 JavaScript API,它允许你监听 HTML 元素的尺寸变化。与监听全局的 window.resize 事件不同,ResizeObserver 专注于监听特定元素。更重要的是,它监听的是元素的内容区域(content box)的尺寸变化,这意味着它能更准确地捕捉到由各种因素引起的尺寸变化。

语法和基本用法

ResizeObserver 的基本使用流程如下:

  1. 创建 ResizeObserver 实例: 使用 new ResizeObserver(callback) 创建一个 ResizeObserver 对象,并传入一个回调函数。这个回调函数会在被监听元素的尺寸发生变化时被调用。

  2. 定义回调函数: 回调函数接收一个 entries 参数,它是一个 ResizeObserverEntry 对象的数组。每个 ResizeObserverEntry 对象都包含了关于被监听元素尺寸变化的信息。

  3. 监听元素: 使用 observe(element) 方法开始监听指定的 HTML 元素。

  4. 停止监听: 当不再需要监听某个元素时,可以使用 unobserve(element) 方法停止监听。可以使用disconnect() 停止所有监听。

代码示例:最简单的监听

const element = document.getElementById('myElement');

const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const { width, height } = entry.contentRect;
    console.log(`元素尺寸变化:宽度 = ${width}px, 高度 = ${height}px`);
    // 在这里执行你的响应式布局逻辑
  });
});

observer.observe(element);

// 在不需要监听时,停止监听
// observer.unobserve(element);
// observer.disconnect();

在这个例子中,我们创建了一个 ResizeObserver 实例,并传入一个回调函数。回调函数会遍历 entries 数组,并从每个 ResizeObserverEntry 对象中提取出 contentRect 属性,该属性包含了元素的宽度和高度。然后,我们就可以在回调函数中执行我们的响应式布局逻辑。

ResizeObserverEntry 对象详解

ResizeObserverEntry 对象包含了关于被监听元素尺寸变化的详细信息。它主要包含以下属性:

属性 类型 描述
target Element 被监听的 HTML 元素。
contentRect DOMRectReadOnly 一个只读的矩形对象,包含了元素的内容区域的尺寸信息(宽度、高度、top、left、bottom、right)。注意:这里的尺寸不包括padding、border和margin。
borderBoxSize ResizeObserverSize[] 一个数组,包含了元素的边框盒(border box)的尺寸信息。在大多数情况下,这个数组只有一个元素。
contentBoxSize ResizeObserverSize[] 一个数组,包含了元素的内容盒(content box)的尺寸信息。在大多数情况下,这个数组只有一个元素。
devicePixelContentBoxSize ResizeObserverSize[] 一个数组,包含了元素的内容盒(content box)的尺寸信息,以设备像素为单位。在大多数情况下,这个数组只有一个元素。

注意: borderBoxSizecontentBoxSizedevicePixelContentBoxSize 属性返回的是一个数组,这是为了支持未来的扩展,例如支持多列布局。但在大多数情况下,这个数组只有一个元素。

代码示例:使用 contentRect 和 borderBoxSize

const element = document.getElementById('myElement');

const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const { width: contentWidth, height: contentHeight } = entry.contentRect;
    const borderBoxSize = entry.borderBoxSize[0];
    const { inlineSize: borderWidth, blockSize: borderHeight } = borderBoxSize;

    console.log(`内容区域:宽度 = ${contentWidth}px, 高度 = ${contentHeight}px`);
    console.log(`边框盒子:宽度 = ${borderWidth}px, 高度 = ${borderHeight}px`);

    // 在这里执行你的响应式布局逻辑
  });
});

observer.observe(element);

在这个例子中,我们同时使用了 contentRectborderBoxSize 属性来获取元素的尺寸信息。contentRect 包含了内容区域的尺寸,而 borderBoxSize 包含了边框盒的尺寸。你可以根据实际需求选择合适的属性。

ResizeObserver 的性能优化

ResizeObserver 在性能方面做了很多优化,以避免频繁的回调函数执行带来的性能问题。

  1. 批量处理: ResizeObserver 会将多个尺寸变化合并成一个回调函数执行。这意味着,如果在短时间内发生了多次尺寸变化,ResizeObserver 只会执行一次回调函数,并将所有的变化信息传递给回调函数。

  2. 异步执行: ResizeObserver 的回调函数是异步执行的。这意味着,回调函数不会阻塞主线程,从而避免了页面卡顿。

  3. 避免死循环: ResizeObserver 会自动避免由于回调函数中的代码引起的死循环。如果你在回调函数中修改了元素的尺寸,ResizeObserver 会自动停止监听,以避免无限循环。

代码示例:利用 requestAnimationFrame 优化回调函数

为了进一步优化性能,我们可以结合 requestAnimationFrame 来使用 ResizeObserverrequestAnimationFrame 可以在浏览器下一次重绘之前执行回调函数,这可以确保我们的布局逻辑在页面渲染之前执行,从而避免页面闪烁。

const element = document.getElementById('myElement');

const observer = new ResizeObserver(entries => {
  requestAnimationFrame(() => {
    entries.forEach(entry => {
      const { width, height } = entry.contentRect;
      console.log(`元素尺寸变化:宽度 = ${width}px, 高度 = ${height}px`);
      // 在这里执行你的响应式布局逻辑
    });
  });
});

observer.observe(element);

在这个例子中,我们将回调函数放在 requestAnimationFrame 的回调函数中执行。这样可以确保我们的布局逻辑在页面渲染之前执行,从而提高页面的性能。

ResizeObserver 的应用场景

ResizeObserver 在响应式布局中有很多应用场景,例如:

  1. 自适应容器: 根据容器的尺寸动态调整内部元素的布局。例如,当容器的宽度小于某个阈值时,将内部元素垂直排列;当容器的宽度大于某个阈值时,将内部元素水平排列。

  2. 文本截断: 根据容器的尺寸动态截断文本,以避免文本溢出。

  3. 图片缩放: 根据容器的尺寸动态缩放图片,以确保图片始终适应容器的大小。

  4. 第三方组件集成: 当集成第三方组件时,可以使用 ResizeObserver 监听组件的尺寸变化,并根据变化调整页面布局。

代码示例:自适应容器

<div id="container">
  <div class="item">Item 1</div>
  <div class="item">Item 2</div>
  <div class="item">Item 3</div>
</div>

<style>
  #container {
    display: flex;
    flex-wrap: wrap; /* 默认水平排列,允许换行 */
  }

  .item {
    width: 200px;
    height: 100px;
    background-color: #eee;
    margin: 10px;
  }
</style>

<script>
  const container = document.getElementById('container');

  const observer = new ResizeObserver(entries => {
    entries.forEach(entry => {
      const { width } = entry.contentRect;

      if (width < 600) {
        container.style.flexDirection = 'column'; // 宽度小于 600px 时,垂直排列
      } else {
        container.style.flexDirection = 'row'; // 宽度大于等于 600px 时,水平排列
      }
    });
  });

  observer.observe(container);
</script>

在这个例子中,我们创建了一个容器,并在容器中放置了三个子元素。我们使用 ResizeObserver 监听容器的尺寸变化。当容器的宽度小于 600px 时,我们将容器的 flex-direction 属性设置为 column,使子元素垂直排列;当容器的宽度大于等于 600px 时,我们将容器的 flex-direction 属性设置为 row,使子元素水平排列。

代码示例:文本截断

<div id="text-container">
  <p id="text">这是一段很长的文本,需要根据容器的宽度进行截断。</p>
</div>

<style>
  #text-container {
    width: 300px;
    border: 1px solid #ccc;
    overflow: hidden; /* 隐藏溢出文本 */
  }

  #text {
    white-space: nowrap; /* 禁止换行 */
  }
</style>

<script>
  const textContainer = document.getElementById('text-container');
  const text = document.getElementById('text');

  const observer = new ResizeObserver(entries => {
    entries.forEach(entry => {
      const { width } = entry.contentRect;

      if (text.scrollWidth > width) {
        // 文本宽度大于容器宽度,进行截断
        text.style.textOverflow = 'ellipsis'; // 使用省略号截断
        text.style.overflow = 'hidden';
      } else {
        // 文本宽度小于等于容器宽度,不进行截断
        text.style.textOverflow = 'clip'; // 取消省略号
        text.style.overflow = 'visible';
      }
    });
  });

  observer.observe(textContainer);
</script>

在这个例子中,我们创建了一个容器,并在容器中放置了一段文本。我们使用 ResizeObserver 监听容器的尺寸变化。当文本的宽度大于容器的宽度时,我们使用 text-overflow: ellipsis 属性截断文本,并使用省略号表示被截断的文本;当文本的宽度小于等于容器的宽度时,我们取消文本的截断。

ResizeObserver 的兼容性

ResizeObserver 的兼容性还是不错的,主流浏览器都支持。

浏览器 版本 支持情况
Chrome 64+ 支持
Firefox 69+ 支持
Safari 13+ 支持
Edge 79+ 支持
Opera 51+ 支持
iOS Safari 13+ 支持
Android Chrome 64+ 支持

对于不支持 ResizeObserver 的浏览器,可以使用 polyfill 来提供兼容性。比如可以使用这个polyfill: https://github.com/que-etc/resize-observer-polyfill

总结

ResizeObserver 是一个非常有用的 API,它可以帮助我们更精准、更高效地监听元素的尺寸变化,从而实现更灵活、更强大的响应式布局。在开发复杂的Web应用时,不妨考虑使用 ResizeObserver 来提升用户体验。

总结表格

特性 ResizeObserver window.resize
监听对象 单个 HTML 元素的内容区域尺寸变化 整个浏览器窗口
触发时机 元素内容区域尺寸实际发生变化时 浏览器窗口尺寸发生变化时
性能 更精准、更高效,避免不必要的回调执行 全局监听,性能损耗较大
适用场景 需要监听特定元素尺寸变化的场景 需要监听整个浏览器窗口尺寸变化的场景
是否异步 异步执行 同步执行

好了,今天的讲座就到这里。希望大家对 ResizeObserver 有了更深入的了解。有什么问题,欢迎随时提问。祝大家编程愉快!

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注