在前端开发中,ResizeObserver
和 IntersectionObserver
都是非常强大的 API,它们提供了高效且非阻塞的方式来监控 DOM 元素的变化。虽然它们都用于观察元素,但它们关注的焦点和使用场景截然不同。
ResizeObserver
什么是 ResizeObserver?
ResizeObserver
API 提供了一种高性能的方式来检测 Element
的内容区域或边框盒(border box)尺寸的变化。它解决了传统上通过监听 window.resize
事件来响应元素尺寸变化所带来的性能问题,因为 window.resize
会在窗口大小变化时触发,而不管具体哪个元素的大小是否真的改变了,并且需要手动计算元素尺寸,效率低下。
ResizeObserver
是异步的,并且在每次重绘前触发。这意味着它不会阻塞主线程,并且能够提供最新的尺寸信息。
如何使用 ResizeObserver?
使用 ResizeObserver
主要分为三步:创建实例、观察元素、停止观察。
构造函数:
new ResizeObserver(callback)
-
callback
: 当被观察元素的尺寸发生变化时,会调用此回调函数。它接收两个参数:entries
: 一个ResizeObserverEntry
对象的数组,每个对象对应一个发生尺寸变化的被观察元素。observer
: 调用此回调的ResizeObserver
实例本身。
实例方法:
observer.observe(targetElement, options)
: 开始观察指定的targetElement
。options
是可选的,可以指定观察的盒模型(如border-box
或content-box
)。observer.unobserve(targetElement)
: 停止观察指定的targetElement
。observer.disconnect()
: 停止观察所有已注册的元素。
ResizeObserverEntry
对象:
entries
数组中的每个 ResizeObserverEntry
对象包含以下属性:
target
: 发生尺寸变化的 DOM 元素。contentRect
: 一个DOMRectReadOnly
对象,表示元素的内容区域的尺寸和位置(不包括 padding, border, margin)。borderBoxSize
: 一个ResizeObserverSize
对象的数组,表示元素的边框盒尺寸。contentBoxSize
: 一个ResizeObserverSize
对象的数组,表示元素的内容盒尺寸。devicePixelContentBoxSize
: 一个ResizeObserverSize
对象的数组,表示元素的内容盒在设备像素下的尺寸。
代码示例:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ResizeObserver 示例</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.resizable-box {
width: 200px;
height: 150px;
background-color: lightblue;
border: 2px solid steelblue;
resize: both; /* 允许用户调整大小 */
overflow: auto;
padding: 10px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14px;
color: #333;
}
.info-panel {
width: 300px;
padding: 15px;
border: 1px solid #ccc;
background-color: #f9f9f9;
border-radius: 5px;
}
.info-panel p {
margin: 5px 0;
}
button {
margin-top: 10px;
padding: 8px 15px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>ResizeObserver 示例</h1>
<div class="resizable-box" id="myBox">
拖动右下角调整大小
<p>当前尺寸:</p>
<p id="boxSize">Width: --px, Height: --px</p>
</div>
<div class="info-panel">
<h2>观察状态</h2>
<p>Box Width: <span id="currentWidth">--</span>px</p>
<p>Box Height: <span id="currentHeight">--</span>px</p>
<button id="disconnectBtn">停止观察</button>
<button id="reconnectBtn" disabled>重新观察</button>
</div>
<script>
const myBox = document.getElementById('myBox');
const boxSizeDisplay = document.getElementById('boxSize');
const currentWidthSpan = document.getElementById('currentWidth');
const currentHeightSpan = document.getElementById('currentHeight');
const disconnectBtn = document.getElementById('disconnectBtn');
const reconnectBtn = document.getElementById('reconnectBtn');
let myObserver; // 用于存储 ResizeObserver 实例
function updateBoxSize(width, height) {
boxSizeDisplay.textContent = `Width: ${width}px, Height: ${height}px`;
currentWidthSpan.textContent = width;
currentHeightSpan.textContent = height;
}
function setupObserver() {
// 创建 ResizeObserver 实例
myObserver = new ResizeObserver(entries => {
for (let entry of entries) {
// entry.target 是被观察的元素
// entry.contentRect 提供了元素的尺寸信息
const { width, height } = entry.contentRect;
console.log(`Element ${entry.target.id} resized to: ${width}x${height}`);
updateBoxSize(Math.round(width), Math.round(height));
}
});
// 开始观察 myBox 元素
myObserver.observe(myBox);
console.log('ResizeObserver started observing myBox.');
disconnectBtn.disabled = false;
reconnectBtn.disabled = true;
}
// 初始设置观察器
setupObserver();
// 停止观察按钮事件
disconnectBtn.addEventListener('click', () => {
if (myObserver) {
myObserver.disconnect();
console.log('ResizeObserver disconnected.');
disconnectBtn.disabled = true;
reconnectBtn.disabled = false;
}
});
// 重新观察按钮事件
reconnectBtn.addEventListener('click', () => {
setupObserver();
});
// 页面加载时获取一次初始尺寸
const initialRect = myBox.getBoundingClientRect();
updateBoxSize(Math.round(initialRect.width), Math.round(initialRect.height));
</script>
</body>
</html>
ResizeObserver 的使用场景:
- 响应式组件内部布局: 当一个组件的父容器尺寸发生变化时,组件内部的布局(例如图表、复杂表格、图片画廊)需要随之调整。
ResizeObserver
可以精确地监控组件自身的尺寸变化,从而触发内部的重新渲染或布局调整,而无需监听全局的window.resize
事件。 - 动态内容调整: 例如,一个文本区域根据其内容自动调整高度,或者一个侧边栏根据主内容区域的高度调整自身高度。
- Canvas/SVG 绘图区域自适应: 当
<canvas>
或<svg>
元素的容器大小改变时,需要重新设置画布的尺寸并重绘内容。 - 虚拟滚动/无限滚动列表: 当列表容器的尺寸发生变化时,可能需要重新计算可见区域内的项目数量或调整滚动条。
- 避免布局抖动 (Layout Thrashing): 通过异步回调,
ResizeObserver
有助于避免在频繁的尺寸变化操作中引起不必要的布局计算和重绘,从而提高性能。
IntersectionObserver
什么是 IntersectionObserver?
IntersectionObserver
API 提供了一种异步且非阻塞的方式来检测目标元素与其祖先元素或文档视口(viewport)之间交叉状态的变化。简单来说,它能判断一个元素是否进入或离开了可见区域,以及它与可见区域交叉的比例。
它旨在解决传统上通过监听 scroll
事件并频繁调用 getBoundingClientRect()
来判断元素可见性所带来的性能问题。IntersectionObserver
将这些计算工作交给浏览器本身处理,并且只在交叉状态发生变化时才触发回调,极大地提升了性能。
如何使用 IntersectionObserver?
构造函数:
new IntersectionObserver(callback, options)
-
callback
: 当目标元素的交叉状态发生变化时,会调用此回调函数。它接收两个参数:entries
: 一个IntersectionObserverEntry
对象的数组,每个对象对应一个发生交叉状态变化的被观察元素。observer
: 调用此回调的IntersectionObserver
实例本身。
-
options
(可选): 一个配置对象,用于定义观察器如何检测交叉。root
: 用于观察的根元素。默认为浏览器视口。必须是目标元素的祖先元素。rootMargin
: 根元素的外边距。可以像 CSS 的margin
属性一样设置,例如"10px 20px 30px 40px"
。这会扩大或缩小根元素的判定区域。threshold
: 一个数字或数字数组,表示目标元素可见性变化的百分比。当目标元素的可见比例达到这些阈值时,回调函数就会被触发。例如0
表示目标元素刚进入或离开根元素时触发,1
表示目标元素完全可见时触发,[0, 0.25, 0.5, 0.75, 1]
表示在这些百分比时都触发。
实例方法:
observer.observe(targetElement)
: 开始观察指定的targetElement
。observer.unobserve(targetElement)
: 停止观察指定的targetElement
。observer.disconnect()
: 停止观察所有已注册的元素。
IntersectionObserverEntry
对象:
entries
数组中的每个 IntersectionObserverEntry
对象包含以下属性:
target
: 发生交叉状态变化的 DOM 元素。isIntersecting
: 一个布尔值,表示目标元素当前是否与根元素交叉。intersectionRatio
: 目标元素当前可见部分的比例(0.0 到 1.0)。boundingClientRect
: 目标元素的边界矩形信息。intersectionRect
: 目标元素与根元素交叉部分的边界矩形信息。rootBounds
: 根元素的边界矩形信息。time
: 发生交叉变化的 Unix 时间戳。
代码示例:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IntersectionObserver 示例</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
height: 2000px; /* 增加页面高度以便滚动 */
display: flex;
flex-direction: column;
align-items: center;
}
.header {
width: 100%;
background-color: #333;
color: white;
padding: 20px;
text-align: center;
position: sticky;
top: 0;
z-index: 100;
}
.spacer {
height: 500px;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
color: #666;
width: 100%;
}
.lazy-image {
width: 300px;
height: 200px;
background-color: #ddd;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #555;
font-size: 18px;
border: 1px dashed #999;
transition: background-color 0.3s ease;
}
.lazy-image.loaded {
background-color: lightgreen;
}
.info-box {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 15px;
border-radius: 5px;
font-size: 14px;
z-index: 1000;
}
</style>
</head>
<body>
<div class="header">页面顶部 (滚动查看效果)</div>
<div class="spacer">向下滚动</div>
<div class="lazy-image" data-src="https://via.placeholder.com/300x200/FF5733/FFFFFF?text=Image+1">
图片 1 (未加载)
</div>
<div class="lazy-image" data-src="https://via.placeholder.com/300x200/33FF57/FFFFFF?text=Image+2">
图片 2 (未加载)
</div>
<div class="spacer">继续向下滚动</div>
<div class="lazy-image" data-src="https://via.placeholder.com/300x200/3357FF/FFFFFF?text=Image+3">
图片 3 (未加载)
</div>
<div class="lazy-image" data-src="https://via.placeholder.com/300x200/FF33A1/FFFFFF?text=Image+4">
图片 4 (未加载)
</div>
<div class="spacer">页面底部</div>
<div class="info-box">
<p>图片 1: <span id="status1">未加载</span></p>
<p>图片 2: <span id="status2">未加载</span></p>
<p>图片 3: <span id="status3">未加载</span></p>
<p>图片 4: <span id="status4">未加载</span></p>
</div>
<script>
const lazyImages = document.querySelectorAll('.lazy-image');
const statusSpans = {
'Image 1': document.getElementById('status1'),
'Image 2': document.getElementById('status2'),
'Image 3': document.getElementById('status3'),
'Image 4': document.getElementById('status4')
};
const observerOptions = {
root: null, // 默认是视口
rootMargin: '0px', // 默认是0
threshold: 0.1 // 当元素10%可见时触发回调
};
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
const imgElement = entry.target;
const imgSrc = imgElement.dataset.src;
const imgName = imgElement.textContent.trim();
if (entry.isIntersecting) {
// 元素进入视口
if (!imgElement.classList.contains('loaded')) {
console.log(`${imgName} 进入视口, 交叉比例: ${entry.intersectionRatio.toFixed(2)}`);
imgElement.style.backgroundImage = `url(${imgSrc})`;
imgElement.textContent = `${imgName} (已加载)`;
imgElement.classList.add('loaded');
statusSpans[imgName].textContent = '已加载';
observer.unobserve(imgElement); // 一旦加载,停止观察
}
} else {
// 元素离开视口 (如果之前加载过,这里不会再触发,因为已经unobserve)
console.log(`${imgName} 离开视口`);
statusSpans[imgName].textContent = '未加载'; // 实际懒加载不会这样,这里仅为演示
}
});
}, observerOptions);
lazyImages.forEach(image => {
imageObserver.observe(image);
});
</script>
</body>
</html>
IntersectionObserver 的使用场景:
- 图片和视频的懒加载: 只有当图片或视频进入用户视口时才加载其资源,节省带宽和提高页面加载速度。
- 无限滚动 (Infinite Scrolling): 当用户滚动到页面底部时,检测到"加载更多"元素进入视口,然后自动加载更多内容。
- 广告可见性检测: 确定广告是否进入用户视口以及停留时间,用于广告效果统计。
- 动画触发: 当元素进入视口时触发特定的 CSS 动画或 JavaScript 动画。
- 统计报告: 记录用户在页面上停留的时间和关注的内容区域。
- 导航栏高亮: 根据用户当前滚动到的内容区域,自动高亮对应的导航链接。
ResizeObserver 和 IntersectionObserver 的区别
特性 | ResizeObserver | IntersectionObserver |
---|---|---|
观察目标 | 元素的尺寸变化(宽度、高度) | 元素的可见性(与根元素或视口的交叉状态) |
触发时机 | 当被观察元素的 contentRect 或 borderBoxSize 发生变化时 |
当被观察元素与根元素(视口或指定祖先)的交叉比例达到设定的 threshold 时,或进入/离开根元素时 |
关注点 | 元素自身的几何尺寸 | 元素在容器或视口中的相对位置和可见性 |
回调参数 | entries 包含 contentRect , borderBoxSize 等尺寸信息 |
entries 包含 isIntersecting , intersectionRatio , boundingClientRect , intersectionRect 等交叉信息 |
主要用途 | 响应式组件内部布局、动态内容调整、Canvas/SVG 尺寸适配 | 懒加载、无限滚动、广告可见性、元素入场动画、统计 |
性能优势 | 替代 window.resize 监听元素尺寸变化,更精确、高效 |
替代 scroll 事件监听和 getBoundingClientRect() 计算元素可见性,更高效、不阻塞主线程 |
总结来说:
- 如果你关心一个 元素自己的大小 变了没有,以及它现在的大小是多少,就用
ResizeObserver
。 - 如果你关心一个 元素有没有出现在屏幕上 (或者某个父容器里),以及它出现了多少,就用
IntersectionObserver
。
它们都是现代 Web 开发中优化性能和提升用户体验的重要工具,各自解决不同但同样重要的前端问题。