2020年9月13日

Mutation Observer(ミューテーションオブザーバー)

MutationObserver は、DOM要素を監視し、変更を検出するとコールバックを実行する組み込みオブジェクトです。

まず構文を見て、次に実際のユースケースを調べて、そのようなものがどこで役立つかを見てみましょう。

構文

MutationObserver は使い方が簡単です。

まず、コールバック関数を使用してオブザーバーを作成します。

let observer = new MutationObserver(callback);

そして、それをDOMノードにアタッチします

observer.observe(node, config);

config は、「どのような種類の変更に反応するか」を指定するブール値オプションを持つオブジェクトです。

  • childListnode の直接の子要素の変更、
  • subtreenode のすべての子孫の変更、
  • attributesnode の属性の変更、
  • attributeFilter – 選択した属性のみを監視するための属性名の配列。
  • characterDatanode.data(テキストコンテンツ)を監視するかどうか、

その他のオプション

  • attributeOldValuetrue の場合、属性の古い値と新しい値の両方をコールバックに渡します(下記参照)。そうでない場合は、新しい値のみを渡します(attributes オプションが必要です)。
  • characterDataOldValuetrue の場合、node.data の古い値と新しい値の両方をコールバックに渡します(下記参照)。そうでない場合は、新しい値のみを渡します(characterData オプションが必要です)。

変更後、callback が実行されます。変更は、MutationRecord オブジェクトのリストとして最初の引数に渡され、オブザーバー自体は2番目の引数として渡されます。

MutationRecord オブジェクトには以下のプロパティがあります。

  • type – ミューテーションタイプ。以下のいずれかです。
    • "attributes":属性が変更された
    • "characterData":データが変更された。テキストノードに使用される。
    • "childList":子要素が追加/削除された。
  • target – 変更が発生した場所:"attributes" の場合は要素、"characterData" の場合はテキストノード、"childList" ミューテーションの場合は要素。
  • addedNodes/removedNodes – 追加/削除されたノード。
  • previousSibling/nextSibling – 追加/削除されたノードの前の兄弟と次の兄弟。
  • attributeName/attributeNamespace – 変更された属性の名前/名前空間(XMLの場合)。
  • oldValue – 対応するオプション attributeOldValue/characterDataOldValue が設定されている場合の、属性またはテキストの変更の以前の値のみ。

たとえば、<div>contentEditable 属性があるとします。この属性により、フォーカスして編集することができます。

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(the changes)
});

// observe everything except attributes
observer.observe(elem, {
  childList: true, // observe direct children
  subtree: true, // and lower descendants too
  characterDataOldValue: true // pass old data to callback
});
</script>

このコードをブラウザで実行し、指定された <div> にフォーカスして <b>edit</b> 内のテキストを変更すると、console.log に1つのミューテーションが表示されます。

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // other properties empty
}];

<b>edit</b> を削除するなど、より複雑な編集操作を行うと、ミューテーションイベントに複数のミューテーションレコードが含まれる場合があります。

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // other properties empty
}, {
  type: "characterData"
  target: <text node>
  // ...mutation details depend on how the browser handles such removal
  // it may coalesce two adjacent text nodes "edit " and ", please" into one node
  // or it may leave them separate text nodes
}];

つまり、MutationObserver はDOMサブツリー内のあらゆる変更に反応することができます。

統合のための使用方法

どのような場合に役立つでしょうか?

便利な機能を含むが、望ましくないことも行うサードパーティスクリプト(例:広告 <div class="ads">不要な広告</div> を表示する)を追加する必要がある状況を想像してみてください。

当然のことながら、サードパーティのスクリプトはそれを削除するメカニズムを提供していません。

MutationObserver を使用すると、不要な要素がDOMにいつ表示されるかを検出し、それを削除できます。

サードパーティのスクリプトがドキュメントに何かを追加する他の状況があり、ページを調整したり、何かを動的にサイズ変更したりするために、いつそれが発生するかを検出したい場合があります。

MutationObserver はこれを実装することを可能にします。

アーキテクチャのための使用方法

MutationObserver がアーキテクチャの観点から優れている場合もあります。

プログラミングに関するWebサイトを作成しているとしましょう。当然、記事やその他の資料にはソースコードスニペットが含まれている場合があります。

HTMLマークアップのそのようなスニペットは次のようになります。

...
<pre class="language-javascript"><code>
  // here's the code
  let hello = "world";
</code></pre>
...

読みやすく、同時に美しくするために、Prism.js のようなJavaScript構文強調表示ライブラリをサイトで使用します。上記の例で構文の強調表示を行うには、Prism.highlightElem(pre) を呼び出します。このメソッドは、このような pre 要素の内容を調べ、このページの例のように、色付きの構文の強調表示のための特別なタグとスタイルをこれらの要素に追加します。

この強調表示メソッドはいつ正確に実行する必要がありますか? DOMContentLoaded イベントで実行するか、スクリプトをページの下部に配置できます。DOMの準備ができたら、要素 pre[class*="language"] を検索し、それらに対して Prism.highlightElem を呼び出すことができます。

// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

これまでのところ、すべて簡単ですね? HTMLでコードスニペットを見つけて強調表示します。

次に進みましょう。サーバーから資料を動的にフェッチするとしましょう。そのためのメソッドについては、チュートリアルの後半で学習します。今のところ重要なのは、WebサーバーからHTML記事をフェッチしてオンデマンドで表示することです。

let article = /* fetch new content from server */
articleElem.innerHTML = article;

新しい article HTMLには、コードスニペットが含まれている場合があります。それらに対して Prism.highlightElem を呼び出す必要があります。そうでない場合は、強調表示されません。

動的にロードされた記事の Prism.highlightElem はどこでいつ呼び出す必要がありますか?

次のように、記事をロードするコードにその呼び出しを追加できます。

let article = /* fetch new content from server */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

…しかし、コンテンツ(記事、クイズ、フォーラムの投稿など)をロードする場所がコードにたくさんあると想像してみてください。ロード後にコンテンツのコードを強調表示するために、強調表示の呼び出しをどこにでも配置する必要がありますか?それはあまり便利ではありません。

そして、コンテンツがサードパーティのモジュールによってロードされた場合はどうでしょうか?たとえば、誰かが書いたフォーラムがあり、それがコンテンツを動的にロードしていて、それに構文の強調表示を追加したいとします。サードパーティのスクリプトにパッチを適用するのは好きではありません。

幸いなことに、別のオプションがあります。

MutationObserver を使用して、コードスニペットがページにいつ挿入されたかを自動的に検出し、強調表示できます。

そのため、強調表示機能は1か所で処理し、統合の必要性を軽減します。

動的強調表示のデモ

これが動作例です。

このコードを実行すると、以下の要素の監視が開始され、そこに表示されるコードスニペットが強調表示されます。

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // examine new nodes, is there anything to highlight?

    for(let node of mutation.addedNodes) {
      // we track only elements, skip other nodes (e.g. text nodes)
      if (!(node instanceof HTMLElement)) continue;

      // check the inserted element for being a code snippet
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // or maybe there's a code snippet somewhere in its subtree?
      for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
        Prism.highlightElement(elem);
      }
    }
  }

});

let demoElem = document.getElementById('highlight-demo');

observer.observe(demoElem, {childList: true, subtree: true});

以下に、HTML要素と、innerHTML を使用して動的にそれを埋めるJavaScriptがあります。

前のコード(上記、その要素を観察する)を実行してから、以下のコードを実行してください。 MutationObserver がスニペットをどのように検出して強調表示するかを確認できます。

id="highlight-demo" を持つデモ要素。上記のコードを実行して観察します。

次のコードは、その innerHTML にデータを入力します。これにより、MutationObserver が反応してコンテンツを強調表示します。

let demoElem = document.getElementById('highlight-demo');

// dynamically insert content with code snippets
demoElem.innerHTML = `A code snippet is below:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>Another one:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

これで、監視対象の要素または document 全体のすべての強調表示を追跡できる MutationObserver ができました。 HTMLでコードスニペットを追加/削除することを考えることなく、追加/削除できます。

追加メソッド

ノードの監視を停止するメソッドがあります。

  • observer.disconnect() – 監視を停止します。

監視を停止すると、オブザーバーによってまだ処理されていない変更がある可能性があります。そのような場合は、次を使用します。

  • observer.takeRecords() – 処理されていないミューテーションレコードのリストを取得します。つまり、発生したがコールバックが処理していないレコードです。

これらのメソッドは、次のように一緒に使用できます。

// get a list of unprocessed mutations
// should be called before disconnecting,
// if you care about possibly unhandled recent mutations
let mutationRecords = observer.takeRecords();

// stop tracking changes
observer.disconnect();
...
observer.takeRecords() によって返されたレコードは、処理キューから削除されます。

observer.takeRecords() によって返されたレコードに対して、コールバックは呼び出されません。

ガベージコレクションの相互作用

オブザーバーは、内部でノードへの弱参照を使用します。つまり、ノードがDOMから削除され、到達不能になると、ガベージコレクションできます。

DOMノードが監視されているという事実だけでは、ガベージコレクションを防ぐことはできません。

まとめ

MutationObserver は、属性、テキストコンテンツ、要素の追加/削除など、DOMの変更に反応できます。

コードの他の部分によって導入された変更を追跡したり、サードパーティのスクリプトと統合したりするために使用できます。

MutationObserver はあらゆる変更を追跡できます。設定「何を監視するか」オプションは、最適化に使用され、不要なコールバック呼び出しにリソースを費やすことはありません。

チュートリアルマップ

コメント

コメントする前にこちらをお読みください…
  • 改善のご提案がある場合は、コメントではなく、GitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを、複数行のコードには <pre> タグを使用してください。10行を超える場合は、サンドボックス(plnkrjsbincodepenなど)を使用してください。