← 中文资源

简体中文版本

Intersection Observer

Intersection Observer 在元素相对根视口跨越可见性阈值时异步回调,用帧对齐的通知取代滚动事件里反复读取几何信息的轮询。

阅读英文原文

概念定义

旧模式是滚动监听加逐元素 getBoundingClientRect,布局工作量随事件频率与元素数相乘。Intersection Observer 反转模式:一次性注册元素,引擎在布局与绘制之后计算交集,只在状态变化时回调。

工作机制

观察者配置 root、扩大或收缩根盒的 rootMargin,以及 0 到 1 的阈值列表。条目携带当前比例、包围盒与 isIntersecting。回调在渲染后运行,测量稳定且不会强制布局;一个观察者可高效监视大量目标。

使用场景

首屏以下内容懒加载、区块可见即打点、滚动到底前预取下一页、离屏暂停视频或动画。与 content-visibility 组合跳过渲染,与原生 lazy loading 组合处理普通图片。

常见误区

它并非每次滚动都触发:回调按帧批处理、按阈值跨越驱动。条目是快照,快速滚动会合并跨越——需要统计每次通过的计数器应调阈值或加驻留逻辑。root 可以是任意可滚动祖先或元素,不只视口;给每个元素单独建观察者则会白白增加设置开销。

代码示例

<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>
public class StringBuilderManipulation {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder(64);

        sb.append("Hello");
        sb.append(" ");
        sb.append("World");
        System.out.println("Built: " + sb);

        sb.insert(5, " Java");
        System.out.println("After insert: " + sb);

        sb.delete(5, 10);
        System.out.println("After delete: " + sb);

        sb.replace(0, 5, "Hi");
        System.out.println("After replace: " + sb);

        sb.reverse();
        System.out.println("Reversed: " + sb);

        String csv = String.join(", ",
            "Apple", "Banana", "Cherry", "Date");
        System.out.println("CSV: " + csv);

        String formatted = String.format(
            "Name: %s, Age: %d, Score: %.2f",
            "Alice", 30, 95.5
        );
        System.out.println(formatted);

        String stripped = "  padded  ".strip();
        System.out.println("Stripped: '" + stripped + "'");
    }
}