Resize Observer API(Resize Observer API)
一、为什么需要 Resize Observer
1.1 传统方法的局限性
在 Resize Observer 出现之前,监听元素尺寸变化是一项复杂的任务:
window.resize 事件的问题:
- 只能监听窗口大小变化,无法监听特定元素
- 在滚动时频繁触发
- 无法获取具体哪个元素发生了变化
// 传统方法:定时轮询
function checkElementSize(element, callback) {
let lastWidth = element.offsetWidth;
let lastHeight = element.offsetHeight;
setInterval(() => {
const newWidth = element.offsetWidth;
const newHeight = element.offsetHeight;
if (newWidth !== lastWidth || newHeight !== lastHeight) {
callback({ width: newWidth, height: newHeight });
lastWidth = newWidth;
lastHeight = newHeight;
}
}, 100);
}
getBoundingClientRect 的问题:
- 触发强制同步布局
- 性能开销大
- 需要手动管理轮询
1.2 Resize Observer 的优势
Resize Observer 是专门为观察元素尺寸变化设计的 API:
- 专门针对元素:观察特定元素的尺寸变化
- 异步执行:不在滚动时阻塞主线程
- 浏览器优化:内部使用 CSS containments 优化
- 与 CSS 布局协同:与 CSS 变化(如 flex、grid)无缝配合
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
console.log('Element:', entry.target);
console.log('Content rect:', entry.contentRect);
console.log('New size:', entry.contentRect.width, entry.contentRect.height);
});
});
observer.observe(document.querySelector('.resizable'));
二、API 详解
2.1 基本用法
const observer = new ResizeObserver(callback);
function callback(entries, observer) {
entries.forEach(entry => {
// entry 类型是 ResizeObserverEntry
});
}
observer.observe(element);
observer.unobserve(element);
observer.disconnect();
2.2 ResizeObserverEntry
// ResizeObserverEntry 的属性
entries.forEach(entry => {
// 目标元素
const target = entry.target;
// 内容矩形(border-box 内的区域)
const contentRect = entry.contentRect;
console.log('Width:', contentRect.width);
console.log('Height:', contentRect.height);
console.log('X:', contentRect.x);
console.log('Y:', contentRect.y);
console.log('Top:', contentRect.top);
console.log('Right:', contentRect.right);
console.log('Bottom:', contentRect.bottom);
console.log('Left:', contentRect.left);
});
2.3 边框盒 vs 内容盒
ResizeObserver 默认报告内容盒(content box)的尺寸变化:
// 观察内容盒(默认)
observer.observe(element);
// 观察边框盒(通过 ResizeObserverSize)
observer.observe(element, { box: 'border-box' });
// 观察设备像素比调整后的内容盒
observer.observe(element, { box: 'device-pixel-content-box' });
三、实际应用场景
3.1 响应式组件
// 根据容器宽度自动调整布局
class ResponsiveGrid {
constructor(container, options = {}) {
this.container = container;
this.options = {
minColumnWidth: 200,
gap: 16,
...options
};
this.observer = new ResizeObserver(() => {
this.updateLayout();
});
this.observer.observe(container);
}
updateLayout() {
const containerWidth = this.container.clientWidth;
const gap = this.options.gap;
const minWidth = this.options.minColumnWidth;
// 计算列数
const columns = Math.max(1, Math.floor((containerWidth + gap) / (minWidth + gap)));
// 计算实际列宽
const totalGap = gap * (columns - 1);
const columnWidth = (containerWidth - totalGap) / columns;
this.container.style.gridTemplateColumns = `repeat(${columns}, ${columnWidth}px)`;
this.container.style.gap = `${gap}px`;
}
destroy() {
this.observer.disconnect();
}
}
3.2 文本省略与容器查询
// 根据容器宽度决定文本显示方式
class TextTruncator {
constructor(element, options = {}) {
this.element = element;
this.options = {
lines: 1,
ellipsis: '...',
...options
};
this.observer = new ResizeObserver(() => {
this.checkTruncation();
});
this.observer.observe(element);
}
checkTruncation() {
const width = this.element.clientWidth;
const fontSize = parseFloat(getComputedStyle(this.element).fontSize);
// 估算每行字符数
const charWidth = fontSize * 0.5; // 粗略估计
const charsPerLine = Math.floor(width / charWidth);
// 根据容器宽度决定是否截断
if (charsPerLine < 20) {
this.element.style.whiteSpace = 'nowrap';
this.element.style.overflow = 'hidden';
this.element.style.textOverflow = 'ellipsis';
} else {
this.element.style.whiteSpace = 'normal';
this.element.style.overflow = 'visible';
this.element.style.textOverflow = 'clip';
}
}
destroy() {
this.observer.disconnect();
}
}
3.3 图表自适应
// 图表容器大小变化时重绘
class ResponsiveChart {
constructor(container, chart) {
this.container = container;
this.chart = chart;
this.observer = null;
this.init();
}
init() {
this.observer = new ResizeObserver(
this.debounce((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
this.chart.resize(width, height);
}
});
}, 100)
);
this.observer.observe(this.container);
}
debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
destroy() {
this.observer.disconnect();
}
}
3.4 视频播放器适配
// 根据容器大小选择视频源
class AdaptiveVideoPlayer {
constructor(container, video) {
this.container = container;
this.video = video;
this.observer = null;
this.init();
}
init() {
this.observer = new ResizeObserver(() => {
this.selectVideoSource();
});
this.observer.observe(this.container);
}
selectVideoSource() {
const width = this.container.clientWidth;
// 根据宽度选择视频质量
if (width < 480) {
this.video.src = this.video.dataset.src480;
} else if (width < 720) {
this.video.src = this.video.dataset.src720;
} else if (width < 1080) {
this.video.src = this.video.dataset.src1080;
} else {
this.video.src = this.video.dataset.src4k;
}
}
destroy() {
this.observer.disconnect();
}
}
四、与容器查询的对比
4.1 容器查询(CSS Container Queries)
容器查询是 CSS 的新特性,允许基于父容器尺寸应用样式:
/* 定义容器 */
.card-container {
container-type: inline-size;
container-name: card;
}
/* 根据容器宽度应用样式 */
@container card (min-width: 300px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
4.2 Resize Observer vs 容器查询
| 特性 | Resize Observer | 容器查询 |
|---|---|---|
| 触发时机 | JavaScript 回调 | CSS 规则应用 |
| 用途 | 逻辑处理、DOM 操作 | 样式变化 |
| 灵活性 | 可以执行任意 JavaScript | 仅限于 CSS 声明 |
| 性能 | 异步、浏览器优化 | CSS 引擎优化 |
| 浏览器支持 | 良好 | 逐渐支持中 |
4.3 配合使用
// Resize Observer 处理逻辑
// 容器查询处理样式
class SmartComponent {
constructor(element) {
this.element = element;
this.observer = null;
// 添加 container 类
this.element.classList.add('container');
// Resize Observer 处理数据相关的逻辑
this.observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
const width = entry.contentRect.width;
// 更新 data 属性,触发 CSS 容器查询
this.element.dataset.width = Math.floor(width);
// 同时执行 JavaScript 逻辑
this.updateVirtualList(width);
});
});
this.observer.observe(this.element);
}
updateVirtualList(width) {
// 根据宽度调整虚拟列表
if (width > 600) {
this.virtualList.setColumns(4);
} else if (width > 400) {
this.virtualList.setColumns(2);
} else {
this.virtualList.setColumns(1);
}
}
destroy() {
this.observer.disconnect();
}
}
五、性能最佳实践
5.1 共享观察者
// 多个元素使用同一个观察者
class SharedResizeObserver {
constructor(callback) {
this.callback = callback;
this.observer = new ResizeObserver((entries) => {
this.callback(entries);
});
this.targets = new WeakSet();
}
observe(element) {
if (!this.targets.has(element)) {
this.targets.add(element);
this.observer.observe(element);
}
}
unobserve(element) {
this.targets.delete(element);
this.observer.unobserve(element);
}
disconnect() {
this.observer.disconnect();
this.targets.clear();
}
}
5.2 防抖处理
class DebouncedResizeObserver {
constructor(callback, delay = 100) {
this.callback = callback;
this.delay = delay;
this.observer = null;
this.timeouts = new Map();
this.init();
}
init() {
this.observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
// 清除之前的 timeout
if (this.timeouts.has(entry.target)) {
clearTimeout(this.timeouts.get(entry.target));
}
// 设置新的 timeout
this.timeouts.set(entry.target, setTimeout(() => {
this.callback(entry);
this.timeouts.delete(entry.target);
}, this.delay));
});
});
}
observe(element) {
this.observer.observe(element);
}
disconnect() {
this.observer.disconnect();
this.timeouts.forEach(timeout => clearTimeout(timeout));
this.timeouts.clear();
}
}
5.3 只在尺寸实际变化时处理
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
const prev = entry.target.dataset.prevSize;
if (prev) {
const [prevWidth, prevHeight] = prev.split(',').map(Number);
// 只在尺寸实际变化时处理
if (prevWidth !== Math.round(width) || prevHeight !== Math.round(height)) {
entry.target.dataset.prevSize = `${width},${height}`;
onResize(entry);
}
} else {
entry.target.dataset.prevSize = `${width},${height}`;
onResize(entry);
}
});
});
function onResize(entry) {
console.log('Size changed:', entry.contentRect.width, entry.contentRect.height);
}
六、浏览器支持与降级
6.1 检测支持
function isResizeObserverSupported() {
return 'ResizeObserver' in window;
}
if (!isResizeObserverSupported()) {
// 使用轮询降级
function pollResize(element, callback) {
let lastWidth = element.offsetWidth;
let lastHeight = element.offsetHeight;
setInterval(() => {
const newWidth = element.offsetWidth;
const newHeight = element.offsetHeight;
if (newWidth !== lastWidth || newHeight !== lastHeight) {
callback({ width: newWidth, height: newHeight });
lastWidth = newWidth;
lastHeight = newHeight;
}
}, 100);
}
}