DOM(要素)の変更や更新を検知するベストな方法〜MutationObserver APIの使い方【JavaScriptガイド】

JavaScript

最近、仕事でDOMが変更されたときにコードを発火したい場面が多く、クリック時やajaxで内容の更新などのタイミングでその都度似たようなメソッドを仕込むのが面倒になることがありました。
特に大量のiframeを扱う必要があったので、1ヶ所1ヶ所にコードを追記するのが非常に効率が悪く、メンテも億劫になること間違いなしです。そこで、大枠のページにDOMを監視させて、変更や更新があったら都度自動的に実行するような手法を採ることにしました。

UIがインタラクティブになるほど、DOMの更新が頻繁に行われるようになります。MutationObserver APIを使用することで、ブラウザが提供するネイティブなイベントを利用して効率的にDOMの変更を監視することができます。おそらく脱中級者の内容になりますのでしっかり対応できるようにしておくことをおすすめします。

DOMの変更(追加/削除/クラスの付与など)を検知したい!

特定の<div>に新たに<div>が追加されたときやその<div>にclassが追加されたり削除されたりした場合にJSを発火させたいことがよくあります。フレームワークを使わない場合は特に意識的に記述していく必要がありますね。そこで便利なのが、ブラウザ標準のMutationObserver APIです。

MutationObserver APIとは?

MutationObserver APIは、ウェブブラウザのJavaScript環境でDOMの変更を監視するためのネイティブな機能です。このAPIを使用すると、DOMの特定の部分に対する変更をリアルタイムで検知し、必要な処理を行うことができます。従来、DOMの変更を検知するために、定期的にDOMをポーリング(setIntervalやsetTimeoutなどで一定時間で繰り返し監視)する方法もありましたが、MutationObserver APIはこれを効率的に解決します。

MutationObserverは、監視対象のノード変更の種類を指定することで、DOMに対するあらゆる変更をキャプチャします。これにより、UIの動的な更新やユーザーインタラクションに即座に対応することが可能です。

基本的な使い方

MutationObserver APIを利用する際のおおまかな流れは以下の様になります。(めちゃくちゃ簡単……)

  1. MutationObserverの作成
  2. 監視対象の指定と実行

まず、1.の作成ですが、具体的には以下の様なコードになります。

const observer = new MutationObserver((mutations) => { /* 1.作成 */
  mutations.forEach((mutation) => {
    // ここに実行する内容を記載
    console.log(mutation);
  });
});

observerというオブジェクトでMutationObserverを新規作成しているだけですね。
mutationsとありますが、これは「検出された変更をリスト状にしたオブジェクト」になります。リスト状なのでforEachで回しています。

2.については設定と実行を以下の様に行います。

const targetNode = document.getElementById('target');
const config = { attributes: true, childList: true, subtree: true }; /* 2.設定値指定 */
observer.observe(targetNode, config); /* 2.設定・実行 */

MutationObserverを使用する際、observeメソッドに渡すconfigオブジェクトで監視する変更の種類を指定します。configについて詳細は後述します。

MutationObserverオブジェクト.observe(ターゲットのDOM要素, 監視対象の種類);

というように覚えておくと良いでしょう。非常に簡潔に記述できて便利ですね。

監視する変更の種類(config)について

observeメソッドを実行する際に、監視対象の要素に加え、監視する変更の種類を引数として渡すことができるといいましたが、具体的には以下のような指定が可能です。

  • 要素に新しいクラスが追加されたり、既存のクラスが変更されたりした場合など
  • 新しい子要素が追加されたり、既存の子要素が削除されたりした場合
  • 監視対象の要素内部で行われるすべてのDOM変更を検知したい場合
  • class属性やstyle属性の変更のみを監視する場合
  • 属性の変更前の値を比較したい場合
  • テキストコンテンツの変更を監視したい場合
  • テキストの変更前後の内容を比較したい場合

attributes(必須)

attributesは監視する要素の属性の変化(classやstyleなど)の判定の有無を指定することが出来ます。

const config = { attributes: true }; // true or false

childList(必須)

childListは監視対象の要素の子ノードの追加や削除を監視するかどうかを指定します。

const config = { childList: true }; // true or false

subtree(必須)

subtreeは監視対象の要素だけでなく、そのすべての子孫要素に対しても監視を行うかどうかを指定します。

const config = { subtree: true }; // true or false

attributeFilter

attributeFilter特定の属性の変更のみを監視したい場合に、その属性名を配列で指定します。※attributestrueである必要があります。

const config = { attributes: true, attributeFilter: ['class', 'style'] }; // 配列でclassなどを指定

attributeOldValue

attributeOldValueは変更前の属性値を記録するかどうかを指定します。※attributestrueである必要があります。

const config = { attributes: true, attributeOldValue: true }; // true or false

たとえば、以下のように古いクラスと新しいクラス(書き換えたクラス)を記憶しておくことが可能です。

const callback = (mutationsList) => {
  for (const mutation of mutationsList) {
    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
      console.log(`Attribute 'class' was modified.`);
      console.log(`Old value: ${mutation.oldValue}`); // 書き換える前の値
      console.log(`New value: ${targetNode.getAttribute('class')}`); // 書き換えた後の値
    }
  }
};

characterData

characterDataは監視対象のテキストの変更を監視するかどうかを指定します。テキストコンテンツの変更を監視したい場合に有効です。

const config = { characterData: true }; // true or false

characterDataOldValue

characterDataOldValueは変更前のテキストを記録するかどうかを指定します。テキストの比較をしたい時などに有用です。※characterDatatrueである必要があります。

const config = { characterData: true, characterDataOldValue: true }; // true or false

具体的な用法とMutationObserverの注意点

MutationObserverを利用する際は少し注意が必要です。たとえば、「監視対象を出来るだけ絞る」方がアプリケーションのパフォーマンスが落ちないので、対象をなるべく小さくすべきです。範囲が大きすぎるとそれだけ処理に負荷がかかります。

また、「監視しているDOMの中に新たに要素を加えると無限ループに陥る」ので、監視を一旦中止させ、要素の生成後に再開させる必要があります。
簡単なダメな例を挙げると

const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        // 変更処理
        $('#target').append('<div></div>'); // 監視のループが起こりブラウザがクラッシュします……
    });
});

簡潔に記載するためにjQueryで書きましたが、#targetは監視中の要素とすると、監視中に<div>の追加が起こり、さらにそれを監視しているので処理が進まず、ぐるぐると無限ループに陥ります。ブラウザがクラッシュするので注意しましょう……

具体的には以下のように一旦監視を停止してから<div>を追加し、終わったら監視を再開すると良いです。監視対象に<span>が追加された場合に<div>を追加する例です。

const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeName === 'SPAN') { // <span>が監視対象に追加されたら
                    observer.disconnect(); // 監視を一時停止
                    // 変更処理
                    $('#target').append('<div></div>');
                    observer.observe(targetNode, config); // 監視を再開
                }
            });
        }
    });
});

監視対象に<span>が追加された場合に、disconnect()メソッドで監視を停止し、<div>を追加してからobserver()メソッドを使って再開させています。無限ループは結構なりやすいのでハマらないようにしましょう……自戒を込めて。

まとめ

MutationObserver APIを使うことで、DOMの変更を効率的にリアルタイムで検知し、必要な処理を行うことができます。ページ全体でDOMを監視し、変更があれば自動的に処理を実行する手法をお覚えておけば、イベント個別に処理を記載せずに一括管理することが出来るようになります。意外と知られていない(ような気がする?)ので、バンバン使って効率的なソースコードにしていきましょう!

タイトルとURLをコピーしました