概念定义
组件常常要适应“自己的盒子”变化——flex 重排、侧栏收起、字体加载——而不是窗口变化。Resize Observer 在布局稳定的时机上报每个元素的尺寸变化,无需定时器猜测或在无关处理器里读几何。
工作机制
observe 注册目标后,凡布局变化改变了其盒子,观察者就在下一帧排队回调。回调收到带 contentRect 与盒尺寸的条目。若回调内的变更再次改变被观察元素尺寸,会触发后续批次,循环检测会以告警而非无限旋转的方式兜底。
使用场景
图表、canvas、虚拟化表格、嵌入组件,以及必须适配任意宿主的设计系统组件。声明式场景可交给原生容器查询,观察器仍是 canvas 后备存储尺寸这类命令式工作的利器。
常见误区
回调里先读后写可能让兄弟观察器之间级联成 resize 循环;影响布局的写操作应 defer 到 rAF。observe 注册时会立即触发一次,只期待后续变化的代码会意外。帧对齐意味着快速的中间尺寸会被合并成一次通知。
代码示例
/* Grid template areas */
.page-layout {
display: grid;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 250px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
gap: 1rem;
}
.page-layout > header { grid-area: header; }
.page-layout > nav { grid-area: nav; }
.page-layout > main { grid-area: main; }
.page-layout > aside { grid-area: aside; }
.page-layout > footer { grid-area: footer; }
/* Auto-fit responsive grid */
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
/* 12-column grid system */
.grid-12 {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
.grid-12 .col-6 { grid-column: span 6; }
.grid-12 .col-4 { grid-column: span 4; }
.grid-12 .col-3 { grid-column: span 3; }
/* Masonry-like layout */
.masonry {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 10px;
gap: 1rem;
}
.masonry .item-tall { grid-row: span 30; }
.masonry .item-short { grid-row: span 20; }
/* Responsive grid with media query */
@media (max-width: 768px) {
.page-layout {
grid-template-areas:
"header"
"nav"
"main"
"aside"
"footer";
grid-template-columns: 1fr;
}
}
<custom-counter start="10" step="2"></custom-counter>
<script>
class CustomCounter extends HTMLElement {
static get observedAttributes() {
return ['start', 'step'];
}
constructor() {
super();
this._count = 0;
this._step = 1;
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: inline-flex; gap: 0.5rem; align-items: center; }
button { padding: 0.25rem 0.5rem; cursor: pointer; }
.value { font-weight: bold; min-width: 2rem; text-align: center; }
</style>
<button id="dec">-</button>
<span class="value">0</span>
<button id="inc">+</button>
`;
}
connectedCallback() {
this._count = parseInt(this.getAttribute('start') || '0');
this._step = parseInt(this.getAttribute('step') || '1');
this._update();
this.shadowRoot.getElementById('inc')
.addEventListener('click', () => this._increment());
this.shadowRoot.getElementById('dec')
.addEventListener('click', () => this._decrement());
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'step') this._step = parseInt(newVal || '1');
}
disconnectedCallback() {
console.log('Counter removed from DOM');
}
_increment() { this._count += this._step; this._update(); }
_decrement() { this._count -= this._step; this._update(); }
_update() {
this.shadowRoot.querySelector('.value').textContent = this._count;
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this._count }
}));
}
}
customElements.define('custom-counter', CustomCounter);
document.querySelector('custom-counter')
.addEventListener('change', e => console.log('Count:', e.detail.value));
</script>