Appearance
一文吃透 ResizeObserver:它解决了什么问题?如何使用?回调入参 entries 的每个属性(contentRect、borderBoxSize、contentBoxSize、devicePixelContentBoxSize、target)到底代表什么?observe 的 box 选项怎么选?附跨浏览器坑位、性能与 Vue 组合式写法
目录
- 概览
- 快速上手
- API 详解
- 回调参数
entries逐项拆解 - 观察选项
box如何选择 - 常见用法模式(含你的 Split 示例)
- 在 Vue 3 中的优雅使用
- 性能与坑位
- 调试清单
- 速查表
概览
ResizeObserver 用于观察某个元素的盒子尺寸变化(而不是窗口尺寸)。与传统 window.onresize 不同,它能监听任意元素的宽高变化(内容尺寸、边框盒、设备像素盒),典型场景:
- 自适应组件(图表、编辑器、分栏、虚拟列表)
- 容器查询的补充(了解容器尺寸,触发内部布局变化)
- 拖拽、折叠面板、Split-pane 布局联动
快速上手
ts
// 复杂逻辑:创建观察者,回调在元素尺寸变化时被批处理调用(微任务之后、绘制前后)
const ro = new ResizeObserver((entries, observer) => {
for (const entry of entries) {
// entry 是 ResizeObserverEntry
const el = entry.target as HTMLElement;
// contentRect 是旧接口,读内容盒的宽高(不含 padding + border)
const { width, height } = entry.contentRect;
// 新接口,读“内容盒”尺寸(数组是为多片段布局准备的)
const contentInline = entry.contentBoxSize?.[0]?.inlineSize;
const contentBlock = entry.contentBoxSize?.[0]?.blockSize;
console.log(
"元素",
el,
"内容宽高",
width,
height,
"inline/block",
contentInline,
contentBlock
);
}
});
// 开始观察某元素(默认观察 content-box)
ro.observe(document.querySelector("#box")!);
// 停止观察某元素
// ro.unobserve(document.querySelector('#box')!)
// 断开所有观察
// ro.disconnect()API 详解
构造函数
ts
const ro = new ResizeObserver(callback);callback(entries, observer):尺寸变化被批处理后调用。不要在回调里同步反复修改被观察元素尺寸,否则可能触发ResizeObserver loop limit exceeded。
实例方法
observe(target: Element, options?: { box?: 'content-box' | 'border-box' | 'device-pixel-content-box' })unobserve(target: Element)disconnect()
box含义见后文;不传则等价于{ box: 'content-box' }。
回调参数 entries 逐项拆解
回调签名:
ts
type ResizeObserverCallback = (
entries: ResizeObserverEntry[],
observer: ResizeObserver
) => void;entries 是一组 ResizeObserverEntry,每个 entry 对应一个发生了尺寸变化的元素。
ResizeObserverEntry 的属性:
target: Element被观察的元素(你的代码里就是div.split-horizontal.w-full)。contentRect: DOMRectReadOnly(旧接口,仍被广泛使用)
- 表示**内容盒(content-box)**的矩形:
x, y, width, height, top, right, bottom, left - 不含
padding/border/margin。 - 局限:只能读内容盒;新接口能更灵活区分不同盒模型。
contentBoxSize: ResizeObserverSize[](新接口)
表示内容盒的尺寸集合(数组是为多片段容器准备的,大多数情况下只有一项)。
每一项是
ResizeObserverSize:inlineSize: 写入方向的尺寸(在水平书写中等同 width)blockSize: 块方向的尺寸(在水平书写中等同 height)
优点:与书写模式(writing-mode)无关,比
width/height更国际化。
borderBoxSize: ResizeObserverSize[](新接口)
- 表示边框盒(包括
padding+border)的尺寸(多数情况仅一项)。 - 当你需要与 CSS
box-sizing: border-box;对齐时,应使用它。
devicePixelContentBoxSize: ResizeObserverSize[](新接口)
- 表示设备像素级的内容盒尺寸,已经乘上
devicePixelRatio。 - 渲染位图(如
<canvas>、高 DPI 图表)时很有用,可减少模糊。
兼容性提示:老版 Safari 可能返回
borderBoxSize等为对象而非数组,或没有这些属性。要写兼容代码(见下文)。
观察选项 box 如何选择
在 observe(el, { box }) 里可以指定观测哪种盒子:
content-box(默认):内容区域尺寸变化才触发;适合根据内容变化重排。border-box:包含padding + border;与box-sizing: border-box设计一致,常用于与布局尺寸对齐。device-pixel-content-box:内容盒的设备像素尺寸(乘上 DPR);适合 Canvas/图表等需要像素级精度的场景。
选择建议:
- 你的场景如果是计算分栏/剩余宽度,通常选
border-box更直观; - 需要高分屏绘图 → 选
device-pixel-content-box; - 默认不指定也能用,但读值时记得从对应的
*BoxSize里拿。
常见用法模式(含你的 Split 示例)
模式 1:读取“内容盒”宽高(兼容写法)
ts
function getContentSize(entry: ResizeObserverEntry) {
// 复杂逻辑:优先用新接口;兼容老浏览器回退到 contentRect
const cb = Array.isArray(entry.contentBoxSize)
? entry.contentBoxSize[0]
: (entry as any).contentBoxSize; // 某些 Safari 旧版是对象
const width = cb?.inlineSize ?? entry.contentRect.width;
const height = cb?.blockSize ?? entry.contentRect.height;
return { width, height };
}模式 2:读取“边框盒”宽高(与 border-box 对齐)
ts
function getBorderBoxSize(entry: ResizeObserverEntry) {
const bb = Array.isArray(entry.borderBoxSize)
? entry.borderBoxSize[0]
: (entry as any).borderBoxSize;
// 回退到 contentRect 以避免 undefined
const width = bb?.inlineSize ?? entry.contentRect.width;
const height = bb?.blockSize ?? entry.contentRect.height;
return { width, height };
}模式 3:高 DPI Canvas 自适应
ts
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const dpcb = Array.isArray(entry.devicePixelContentBoxSize)
? entry.devicePixelContentBoxSize[0]
: (entry as any).devicePixelContentBoxSize;
const dpr = window.devicePixelRatio || 1;
const cssW = entry.contentRect.width;
const cssH = entry.contentRect.height;
const pixelW = dpcb?.inlineSize ?? Math.round(cssW * dpr);
const pixelH = dpcb?.blockSize ?? Math.round(cssH * dpr);
// 复杂逻辑:canvas 实际像素用设备像素,样式尺寸保持 CSS 像素一致
canvas.width = pixelW;
canvas.height = pixelH;
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
}
});
ro.observe(canvas, { box: "device-pixel-content-box" });模式 4:你的 Split-pane 场景
ts
// 复杂逻辑:观察容器宽度变化,动态设置左右面板尺寸
const horizontalSplitObserver = new ResizeObserver((entries) => {
const entry = entries[0];
// 用 border-box 保证与布局占位一致
const { width: containerWidth } = getBorderBoxSize(entry);
if (horizontalSplit) {
horizontalSplit.setSizes([
sidebarPanelWidth.value,
containerWidth - sidebarPanelWidth.value,
]);
}
});
horizontalSplitObserver.observe(
document.querySelector(".split-horizontal.w-full") as Element,
{ box: "border-box" }
);在 Vue 3 中的优雅使用
ts
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue'
const containerRef = ref<HTMLElement | null>(null)
let ro: ResizeObserver | null = null
// 复杂逻辑:封装读取尺寸的兼容工具
function readBorderBox(entry: ResizeObserverEntry) {
const bb = Array.isArray(entry.borderBoxSize)
? entry.borderBoxSize[0]
: (entry as any).borderBoxSize
const width = bb?.inlineSize ?? entry.contentRect.width
const height = bb?.blockSize ?? entry.contentRect.height
return { width, height }
}
onMounted(() => {
ro = new ResizeObserver((entries) => {
const entry = entries[0]
const { width } = readBorderBox(entry)
// 复杂逻辑:这里做布局联动(如 setSizes / 重新渲染图表)
// updateLayout(width)
})
if (containerRef.value) {
ro.observe(containerRef.value, { box: 'border-box' })
}
})
onBeforeUnmount(() => {
ro?.disconnect()
ro = null
})
</script>
<template>
<div ref="containerRef" class="w-full h-full">
<!-- your content -->
</div>
</template>性能与坑位
批处理调用 回调不会为每次微小变化都同步触发,浏览器会合并/节流。避免在回调内做昂贵计算,可加自节流/防抖。
循环限制错误 如果回调中修改了被观察元素的尺寸,且导致持续触发,会报:
ResizeObserver loop limit exceeded 解决:在
requestAnimationFrame、setTimeout(0)或下一个 tick 再修改 DOM;或把变更施加到非被观察元素。老浏览器兼容
- 旧 Safari 可能将
*BoxSize暴露为对象而不是数组;或缺失这些属性(仅有contentRect)。 - 写 Array/对象双兼容 与
contentRect回退 的读取逻辑。
- 旧 Safari 可能将
避免多余观察 同一个元素不要重复
observe;同页面过多观察者需注意内存与回调开销。与 MutationObserver 区分
ResizeObserver关注尺寸;MutationObserver关注DOM 结构/属性;IntersectionObserver关注可见性/交叉状态。 目标不同,不可互代。
调试清单
- ✅ 回调是否触发?(尝试改变容器尺寸,或给目标元素加边框确认)
- ✅
observe的元素是否可渲染且在文档中? - ✅ 读取的是哪个盒模型?(
contentRectvsborderBoxSize) - ✅ 是否有循环更改导致 loop exceeded?
- ✅ 是否做了兼容读取(数组/对象/回退到
contentRect)? - ✅ 在组件卸载时
disconnect()了吗?
速查表
构造:
ts
new ResizeObserver((entries, observer) => { ... })Entry 属性:
target: ElementcontentRect: DOMRectReadOnly➜ 内容盒矩形(旧)contentBoxSize: ResizeObserverSize[]borderBoxSize: ResizeObserverSize[]devicePixelContentBoxSize: ResizeObserverSize[]
ResizeObserverSize:
inlineSize(水平书写 ≈width)blockSize(水平书写 ≈height)
方法:
ts
ro.observe(el, {
box: "content-box" | "border-box" | "device-pixel-content-box",
});
ro.unobserve(el);
ro.disconnect();小结
- entries[0].contentRect:内容盒
width/height(旧、通用)。 - entries[0].contentBoxSize[0].inlineSize/blockSize:内容盒 size(新、书写模式友好)。
- entries[0].borderBoxSize[0]...:边框盒 size(与
border-box布局一致)。 - entries[0].devicePixelContentBoxSize[0]...:设备像素级内容盒(高 DPI 渲染)。
在你的分栏(Split)场景:
选
border-box+ 兼容读取 + 异步修改尺寸(必要时requestAnimationFrame),最稳。