← 中文资源

简体中文版本

Resize Observer

Resize Observer 以帧批量回调的形式上报元素级尺寸变化,取代窗口 resize 监听加 getBoundingClientRect 轮询的响应式组件方案。

阅读英文原文

概念定义

组件常常要适应“自己的盒子”变化——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>