👁️‍🗨️

MutationObserver について理解を深める

に公開

onChangeonClickquerySelectorでは検出できない変化がある。
querySelector()は便利な関数ですが欠点があります。それはノードの変化に対応していないということです。getElementByID()getElementsByClassName()などはページが読み込まれた後、動的に作られた要素にも取得できまです。しかし複雑な条件を指定して要素を取得するのは難しい

// 監視対象の要素を設定。ここでは <body> を監視する
const targetNode = document.body;

// オブザーバの設定
const config = {
    childList: true,
    subtree: true
};

// コールバック関数の定義
const callback = (mutationsList, observer) => {
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('子ノードが変更されました!');
        }
    }
};

// MutationObserverのインスタンスを作成
const observer = new MutationObserver(callback);
// 監視を開始
observer.observe(targetNode, config);

// 必要がなくなったら監視を停止
// observer.disconnect();

MutationObserverはDOMの変更を監視するものですが、DOMのどの部分の変更を監視するのかをオプションで記述する必要があります。次の3つのオプションのうち、一つ以上をtrueにします。

オプション 説明
childList boolean 着目している要素の子要素の増減のみを検出
attributes boolean 属性値の変化を検出
characterData boolean テキストノード、コメントノードの内容の変化を検出

このほかのオプション項目に subtreというのもあります。childListでは変化の検出は子要素のみでしたが subtreeをtrueにすると子要素だけでなく孫要素、ひ孫要素、...など、子要素より深い子孫要素の変化も検出できます。subtreeだけでなく他にもオプションがあるので表にして整理しておきます。

オプション 説明
subtree boolean 子要素より深い位置にあるの子孫要素の変化も監視対象にする。
attributeFilter Array このオプションに記述した属性のみを監視する。特定の属性の変更だけを監視する場合に有用。このオプションを使うにはattributes属性をtrueにする必要がある。
attributeOldValue boolean 属性が変更される前の値を取得できるようになる。MutationRecordオブジェクトのtypeattributesのときoldValueプロパティでアクセスできる。このオプションを使うにはattributes属性をtrueにする必要がある。
characterDataOldValue boolean 変更前のテキストの内容を取得できるようになる。MutationRecordオブジェクトのtypecharacterDataのときoldValueプロパティでアクセスできる。このオプションを使うにはcharacterData属性をtrueにする必要がある。

MutationObserverのコールバック関数に渡されるオブジェクトはMutationRecordという読み込み専用のオブジェクトの配列です。MutationRecordは監視対象のDOMに生じた変更情報が格納していて、このオブジェクトにアクセスすると追加された要素、削除された要素などにアクセスできます。

MutationRecordは蓄積ではなく更新される

MutationRecordに格納される変更情報は、蓄積されるのではなく更新されます。理解しやすくするために無限スクロールが実装された画像閲覧サイトを例にして考えてみましょう。検索サイトのHTMLが次のような構造だったとします。

<html lang="en">
  <body>
    <div id="root">
      <img src="img.png" />
      <img src="img.png" />
      <img src="img.png" />
    </div>
    <script type="module" src="./main.js"></script>
  </body>
</html>

サイト訪問時にはrootというid名のdivタグの中に画像が3枚表示されています。サイト訪問後、画面の一番下までスクロールするとdiv<img src="img.png" />という要素が3つ追加されたとします。このときmutationsListには3つのMutationRecordオブジェクトが追加されたことになります。

初回ロード時に一番下までスクロールするとサイトには11-20枚目の画像が表示されて、同時に変更を検知したMutationObserverMutationRecordオブジェクトに11-20枚目の画像情報を格納する。再び、画面の一番下までスクロールすると、サイトには21-30枚目の画像が表示されるのだが、このときMutationRecordオブジェクトに格納されているのは21-30枚目の画像情報だけだ。もう一度一番したまでスクロールすると格納されるのは31-40枚目の画像情報である。このように、MutationRecordオブジェクトに格納される中身は次々更新され続けていくことに注意してほしい。

以下の表はMutationRecordが持つプロパティの一部です。

プロパティ 説明
type String ノードのツリーに対する変更の場合は"childList"、属性値変更の場合は"attributes"CharacterDataノードに対する変更の場合は"characterData"を返す
oldValue string or null 変更前の値をtypeの値に応じて返す。attributesの場合、変更前の属性値を返す。characterDataの場合、変更前のテキストの内容を返す。typechildListの場合、null
target Node 変更が発生したノードを、typeの値に応じて返す。attributesの場合、属性が変更された要素を返す。characterDataの場合、自身のテキスト内容が変更されたノードを返す。childListの場合、子ノードが変更されたノードを返す。
addedNodes NodeList 追加されたノードを返す。追加されたノードがなければ空の NodeListを返す。
removedNodes NodeList 削除されたノードを返す。さされたノードがなければ空の NodeListを返す。
previousSibling Node 追加あるいは削除されたノードの直前にあるノード、もしくはnullを返す
nextSibing Node 追加、あるいは削除されたノードの直後にあるノード、もしくは nullを返す

このように、DOMの変更を監視すると言っても、変更された対象が子ノードなのか、属性値/CSS値なのか、それともCharactorDataなのか3つに大別できます。さらに変更といっても二種類あって、追加されたのか削除されたのかに分けられます。(更新というのはないのか?)

自分が検出したいDOMを検出するための条件を正しく見つけ、条件分岐を行い流れ込むノードをふるいにかけることで、欲しい要素を特定することができます。

自分が検出したいDOMというのは大抵が明確なものです。サイトを表示すると目で見えるからです。DevToolsを使えばそのDOMについての情報はすぐに調べることができます。時間がかかるのは、MutationRecordオブジェクトから得られる情報で、そのDOMを検出するための条件を作ることです。
ここでは追加されたDOMを検出するので、2つのふるいにかける必要があります。1つはtypeプロパティがchildListであるものだけを選別するふるい。2つ目は条件に合ったNodeだけを選別するふるいです。
とはいえtypeのふるいはどっちでもいいような気がする。アーリーリターンで処理を止めれるが、どのくらいの負担になるのかは定かではない。

addedNodesNodeList型なのでループをとってNode型の値を抽出します。欲しいDOMのタグ名、属性値MutationObserverで使うのに便利なNodeのプロパティは次のようなものです。

nodeName、attributes、id、 textContent classList.contains()、data.match()、
nodeName.toLowerCase()、querySelector()、addedNodes.length()、nodeType

tagNameでなくnodeNameなのは、NodeがElementよりも抽象だからかな

上のやつはGithubで使っている例を見た
nodeTypeを使ってた。

https://stackoverflow.com/questions/76199610/why-does-a-mutationobserver-not-work-for-a-next-js-app-that-caches-data

Nodeについては別のページで解説してあるので詳しくはそちらを参考してください。

  • MutationObserver はDOMの変更をリアルタイム追うためのもので、子要素の増減、属性値の変化、テキストの内容の変化を監視するために使う。それ以外では別のものを使う。マウスイベント、フォームイベント、キーボードイベントなど

  • 複数の要素を監視する。(for...of で)
    https://chat.openai.com/share/667141ac-44e7-4661-ba16-436020731337

MutationRecordのドキュメント

無限ループに陥らない

MutationObserverのコールバック関数内に自分でDOMを編集するときは無限ループに陥ってしまう可能性がある。変更を検知->コールバック関数内で要素を変更ー>その変更を検知->コールバックで要素を変更ー>その変更を検知→... と無限ループが発生する。これを防ぐために、コールバック関数内でDOMを編集する前に監視を一時停止し、編集が終われば監視を再開するといい。それぞれdisconnect()observe()メソッドを使うといい。

https://pisuke-code.com/mutation-observer-infinite-loop/

参考サイト

MutationObserver のサンプルコードが書かれた解説記事

JavaScript:要素の中身が変化した時にイベントを起こしたい
十七章第一回 MutationObserver

iframeを検知する

Discussion