Observerを使って要素を監視してみよう!

UI開発者 宇賀

みなさんこんにちは、UI開発者の宇賀です。

何か変更があったら聞きに行かなくても教えて欲しい...。JavaScriptを書いていると、そんな風に思うことが多々あると思います。

以前はオブジェクトのプロパティの変更を検出したり、要素が出力されたのを検出したりすることはなかなか難しかったり、コストがかかるものでした。しかし、今では様々なオブザーバー(観察者、観測者、監視者)がJavaScriptの仕様(正確にはDOMの仕様やECMAScriptの仕様など)には用意されています。

今回はそんなオブザーバーの仲間をいくつか「仕様書へのリンク、大まかなできること、サンプル」の順に紹介したいと思います。

※ この記事に登場するサンプルはブラウザによって正しく動作しない可能性があります。
※ Chrome 64以降のバージョンでの動作を確認しています。

純粋に要素を監視!MutationObserver

WHATWG DOM Living Standard 4.3.1. Interface MutationObserver

  • 対象の要素の属性の変化を監視
  • 対象の要素のテキストノードの変化を監視
  • 対象の要素の子ノードの変化を監視
  • 対象の要素の子孫ノードも監視対象に含める

開発者ツールでコンソールを見ながら「MutationObserverの動作サンプル」内にある要素の属性を操作したり、子要素を挿入、削除したりしてみてください。

MutationObserverの動作サンプル
親要素
子要素
(() => {
    'use strict';

    const target = document.body; // body要素を監視
    const newElement = document.createElement('div'); // 監視対象の要素に挿入する新しい要素
    const observer = new MutationObserver(function (mutations) {
        // observer.disconnect(); // 監視を終了
        console.table(mutations);
    });

    // 監視を開始
    observer.observe(target, {
        attributes: true, // 属性変化の監視
        attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する
        characterData: true, // テキストノードの変化を監視
        characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する
        childList: true, // 子ノードの変化を監視
        subtree: true // 子孫ノードも監視対象に含める
    });

    // APIに観測されるような処理を実施
    target.classList.add('hoge');
    target.classList.add('piyo');
    target.appendChild(newElement);
    target.innerText = 'hogehoge';
})();

要素に何かしら変更があったことを知ることができます。

変更が検出されると、次の内容がコールバック関数の引数に渡されます。

  • 変化の種類(子要素の変化、テキストノードの変化、属性の変化)
  • 変化の影響を受けたNode
  • 追加された要素のNodeList
  • 削除された要素のNodeList
  • 対象要素のpreviousSibling
  • 対象要素のnextSibling
  • 変化があった属性の属性名
  • 変化があった属性の属性の名前空間
  • 変化前の値

MutationObserverは要素の状況が開発者(自分)のコントロール下にないような状況においても、非常に有効なAPIです。

たとえば、「非同期的に情報が自動更新されるWebツールを業務などで使っており、更新を認識して必要な情報があればプッシュ通知させることでより効率的に作業をすすめられるようなブラウザ拡張を開発する場合」や、「サードパーティのウィジェットを読み込まれたタイミングでなにがしかの処理を実行しなければならず、しかしそのウィジェットの読み込みが終わるタイミング、DOMツリーに挿入されるタイミングが不明」のような状況下で力を発揮してくれます。

要素のリサイズを監視!ResizeObserver

WICG Resize Observer 1

  • 要素のサイズの変更を監視
  • サイズが変更された要素のコンテンツエリアの横幅の取得
  • サイズが変更された要素のコンテンツエリアの高さの取得
  • サイズが変更された要素のpadding-left, padding-topの値の取得
  • サイズが変更された要素のpadding-left + コンテンツエリアの横幅
  • サイズが変更された要素のpadding-top + コンテンツエリアの高さ

開発者ツールからCSSを操作したりウィンドウの幅をリサイズしたりして、「ResizeObserverの動作サンプル」内にある要素の横幅を変化させてみてください。

ResizeObserverの動作サンプル
(※ 執筆時点では、Chrome64 以降で動作します)
max-width: 100%;
max-width: 500px;
max-width: 300px;
max-width: 100%
変化していない
max-width: 500px
変化していない
max-width: 300px
変化していない
(() => {
    'use strict';

    const body = document.body; // body要素
    const observer = new ResizeObserver((entries) => {
        console.log(entries); // このインスタンスで監視している要素のうち、受け取った変化が入ったオブジェクト

        // entries.forEach((entry) => {
            // const rect = entry.contentRect;

            // console.group('Element:', entry.target);
            // console.table({
            //     size: `${rect.width}px × ${rect.height}px`,
            //     padding: `${rect.top}px ; ${rect.left}px`
            // });
            // console.groupEnd();

            // observer.unobserve(entry.target); // 1つの要素の監視だけを終了
        // });
        // observer.disconnect(); // このインスタンスに関連づいている監視を全て終了
    });

    // 監視を開始
    observer.observe(body.children[0]);
    observer.observe(body.children[1]);
    observer.observe(body.children[2]);
})();

JavaScriptをスタイリングに利用することを余儀なくされる状況では、windowオブジェクトのresizeイベントにハンドラを登録して画面サイズの変更を検知し、リサイズを想定している全ての要素のスタイルを調整しなおす、といった実装が多いのではないでしょうか。

ResizeObserverを利用することで、要素単位でサイズの変化を検出し、要素単位でレイアウトの調整を行うことができるようになります。

本記事執筆時点ではまだまだWebサイト上では使える段階にないですが、ResizeObserverが使える環境になれば、JavaScriptによるスタイリングのパフォーマンスが向上するかもしれないですね。

要素が範囲内に存在しているかどうかを監視!IntersectionObserver

W3C Intersection Observer

  • 要素とルートの要素(デフォルトはビューポート)との交差している割合を監視
  • 交差を検出するまでのタイムを取得
  • 交差した要素の座標・サイズ情報を取得
  • ルートの要素の座標・サイズ情報を取得
  • 交差しているかどうかのフラグ情報を取得
IntersectionObserverの動作サンプル1
(※ 執筆時点では、Chrome, Firefox, EdgeなどのPCブラウザで動作します)
まだ見えてない

「対象の要素」が画面内に入ったときに「まだ見えてない」が「見えた!」に変化するサンプルです。

対象の要素
IntersectionObserverの動作サンプル2
(※ 執筆時点では、Chrome, Firefox, EdgeなどのPCブラウザで動作します)
ふわっと表示される
(() => {
    'use strict';

    const node = document.querySelector('hoge'); // 監視したい要素
    const observer = new IntersectionObserver((entries) => {
        console.log(entries); // このインスタンスで監視している要素のうち、受け取った変化が入ったオブジェクト

        for (const entry of entries) {
            if (entry.intersectionRatio <= 0.4) {
                return; // ルートと4割以上交差していなかった場合は監視を続行する
            }

            // 4割以上交差していたため処理を行い、監視を終了する
            entry.target.classList.add('is-intersected'); // CSSアニメーションのきっかけになるクラスを付与する例
            observer.unobserve(entry.target); // 1つの要素の監視だけを終了
        }

    }, {
        root: null, // ビューポート
        rootMargin: '0px 0px 0px 0px', // 交差していると判断する領域の拡大
        threshold: [0.2, 0.4, 0.8] // 2割、4割、8割交差したときのみ検出する(デフォルトは0)
    });

    // 監視を開始
    observer.observe(node);
    // observer.disconnect(); // このインスタンスに関連づいている監視を全て終了
})();

これまでwindowのscrollイベントに、要素が画面内に存在するかどうかを判定するハンドラを登録する方法で実装することが多かった印象ですが、IntersectionObserverを使うことでより手軽に「IntersectionObserverの動作サンプル2」のような実装が可能になりました。

また、手軽さだけでなく1pxでもスクロールすればハンドラが呼ばれてしまうscrollイベントに、要素の座標位置関係を計算させるハンドラを登録するよりも負荷がかかりにくく、パフォーマンス面でも改善が望めます。

また、サンプルコードにもあるように、IntersectionObserverオブジェクトからインスタンスを生成する際に渡すオプション(第2引数)で、ルートとする要素や交差判定を行う領域の制御、コールバック関数を呼び出すタイミングの交差量を設定できます。

まとめ

今回ご紹介したObserverは次の3種類です。

  • MutationObserver
  • ResizeObserver
  • IntersectionObserver

要素の変化を監視できたり、要素のリサイズを監視できたり、要素の交差を監視できたり...。このようなObserver系オブジェクトは他にも存在します。昔は苦労していたことも、各種Observerを利用することでいろいろと解決しやすくなってきている印象です。

要所要所で上手に使って、すばらしい監視ライフを送りたいですね!