MutationObserverAPIを理解する
公開: 2022-03-10複雑なWebアプリでは、DOMの変更が頻繁に行われる可能性があります。 その結果、アプリがDOMへの特定の変更に応答する必要がある場合があります。
しばらくの間、DOMへの変更を探すために受け入れられていた方法は、現在は非推奨となっているMutationEventsと呼ばれる機能を使用することでした。 W3Cが承認したMutationEventsの代替品は、MutationObserver APIです。これについては、この記事で詳しく説明します。
古い機能が置き換えられた理由については、古い記事や参考資料の多くで説明されているため、ここでは詳しく説明しません(正義を行うことができないという事実を除いて)。 MutationObserver
APIはほぼ完全なブラウザーサポートを備えているため、必要に応じて、すべてではないにしてもほとんどのプロジェクトで安全に使用できます。
MutationObserverの基本構文
MutationObserver
はさまざまな方法で使用できます。これについてはこの記事の残りの部分で詳しく説明しますが、 MutationObserver
の基本的な構文は次のようになります。
let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);
最初の行は、 MutationObserver()
コンストラクターを使用して新しいMutationObserver
を作成します。 コンストラクターに渡される引数は、修飾するDOMの変更ごとに呼び出されるコールバック関数です。
特定のオブザーバーに適しているかどうかを判断する方法は、上記のコードの最後の行を使用することです。 その行では、 MutationObserver
のobserve()
メソッドを使用して監視を開始しています。 これをaddEventListener()
のようなものと比較できます。 リスナーをアタッチするとすぐに、ページは指定されたイベントを「リッスン」します。 同様に、監視を開始すると、ページは指定されたMutationObserver
の「監視」を開始します。
observe()
メソッドは2つの引数を取ります。ターゲット。変更を監視するノードまたはノードツリーである必要があります。 オプションオブジェクトは、オブザーバーの構成を定義できるMutationObserverInit
オブジェクトです。
MutationObserver
の最後の重要な基本機能はdisconnect()
メソッドです。 これにより、指定された変更の監視を停止でき、次のようになります。
observer.disconnect();
MutationObserverを構成するためのオプション
前述のように、 MutationObserver
のobserve()
メソッドには、 MutationObserver
を記述するためのオプションを指定する2番目の引数が必要です。 可能なすべてのプロパティ/値のペアが含まれている場合、optionsオブジェクトは次のようになります。
let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };
MutationObserver
オプションを設定するときに、これらすべての行を含める必要はありません。 これらは単に参照用に含めているので、使用可能なオプションとそれらが取ることができる値のタイプを確認できます。 ご覧のとおり、1つを除くすべてがブール値です。
MutationObserver
が機能するには、 childList
、 attributes
、またはcharacterData
の少なくとも1つをtrue
に設定する必要があります。そうしないと、エラーがスローされます。 他の4つのプロパティは、これら3つのプロパティの1つと連携して機能します(これについては後で詳しく説明します)。
これまでのところ、概要を説明するために構文をざっと見ただけです。 これらの各機能がどのように機能するかを検討する最良の方法は、さまざまなオプションを組み込んだコード例とライブデモを提供することです。 これが、この記事の残りの部分で行うことです。
childListを使用して子要素への変更を監視する
開始できる最初の最も単純なMutationObserver
は、追加または削除する指定されたノード(通常は要素)の子ノードを検索するものです。 私の例では、HTMLで順序付けされていないリストを作成し、子ノードがこのリスト要素に追加または削除されるたびに知りたいと思います。
リストのHTMLは次のようになります。
<ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>
私のMutationObserver
のJavaScriptには、次のものが含まれています。
let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);
これはコードの一部にすぎません。 簡潔にするために、 MutationObserver
自体を扱う最も重要なセクションを示しています。
いくつかの異なるプロパティを持つMutationRecord
オブジェクトであるmutations
引数をループしていることに注目してください。 この場合、 type
プロパティを読み取り、ブラウザが修飾するミューテーションを検出したことを示すメッセージをログに記録しています。 また、 mList
要素(HTMLリストへの参照)をターゲット要素(つまり、変更を監視する要素)として渡す方法にも注意してください。
- 完全なインタラクティブデモを見る→
ボタンを使用して、 MutationObserver
を開始および停止します。 ログメッセージは、何が起こっているのかを明確にするのに役立ちます。 コード内のコメントもいくつかの説明を提供します。
ここでいくつかの重要な点に注意してください。
- コールバック関数(
mCallback
という名前を付けました。これは、任意の名前を付けることができることを示すためです)は、正常なミューテーションが検出されるたびに、observe()
メソッドが実行された後に起動します。 - 私の例では、修飾するミューテーションの「タイプ」は
childList
なので、MutationRecordをループするときにこれを探すのは理にかなっています。 このインスタンスで他のタイプを探しても何も起こりません(他のタイプは後続のデモで使用されます)。 -
childList
を使用して、ターゲット要素にテキストノードを追加または削除できます。これも対象となります。 したがって、追加または削除される要素である必要はありません。 - この例では、直接の子ノードのみが適格になります。 この記事の後半で、これがすべての子ノード、孫などにどのように適用できるかを示します。
要素の属性の変更を監視する
追跡する可能性のあるもう1つの一般的なタイプのミューテーションは、指定された要素の属性が変更された場合です。 次のインタラクティブなデモでは、段落要素の属性の変更を観察します。
let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
- デモをお試しください→
繰り返しになりますが、わかりやすくするためにコードを省略していますが、重要な部分は次のとおりです。
-
options
オブジェクトはattributes
プロパティを使用しており、true
に設定して、ターゲット要素の属性への変更を検索することをMutationObserver
に通知します。 - ループでテストしているミューテーションタイプは属性であり、この場合に適格となるのは
attributes
だけです。 - また、
mutation
オブジェクトのattributeName
プロパティを使用しています。これにより、変更された属性を確認できます。 - オブザーバーをトリガーすると、オプションとともに、参照によって段落要素が渡されます。
この例では、ボタンを使用して、対象のHTML要素のクラス名を切り替えています。 ミューテーションオブザーバーのコールバック関数は、クラスが追加または削除されるたびにトリガーされます。
文字データの変化を監視する
アプリで探したいもう1つの変更は、文字データの変更です。 つまり、特定のテキストノードに変更されます。 これは、 options
オブジェクトでcharacterData
プロパティをtrue
に設定することによって行われます。 コードは次のとおりです。
let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }
コールバック関数で検索されるtype
がcharacterData
であることに再度注意してください。
- ライブデモを見る→
この例では、 element.childNodes[0]
を介してターゲットとする特定のテキストノードへの変更を探しています。 これは少しハッキーですが、この例ではうまくいきます。 テキストは、段落要素のcontenteditable
属性を介してユーザーが編集できます。
文字データの変化を観察する際の課題
contenteditable
をいじくり回している場合は、リッチテキスト編集を可能にするキーボードショートカットがあることに気付くかもしれません。 たとえば、CTRL-Bはテキストを太字にし、CTRL-Iはテキストを斜体にします。 これにより、テキストノードが複数のテキストノードに分割されるため、元のノードの一部と見なされているテキストを編集しない限り、 MutationObserver
が応答を停止することに気付くでしょう。
また、すべてのテキストを削除すると、 MutationObserver
がコールバックをトリガーしなくなることも指摘しておく必要があります。 これは、テキストノードが消えると、ターゲット要素が存在しなくなるためだと思います。 これに対抗するために、私のデモでは、テキストが削除されると監視を停止しますが、リッチテキストショートカットを使用すると少し粘着性があります。
ただし、心配しないでください。この記事の後半で、これらの癖の多くに対処することなく、 characterData
オプションを使用するためのより良い方法について説明します。
指定された属性の変更を監視する
以前、指定された要素の属性の変更を監視する方法を示しました。 その場合、デモはクラス名の変更をトリガーしますが、指定された要素の任意の属性を変更できた可能性があります。 しかし、他の属性を無視しながら、1つ以上の特定の属性への変更を観察したい場合はどうなりますか?
option
オブジェクトのオプションのattributeFilter
プロパティを使用してこれを行うことができます。 次に例を示します。
let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
上に示したように、 attributeFilter
プロパティは、監視する特定の属性の配列を受け入れます。 この例では、 MutationObserver
は、 hidden
、 contenteditable
、またはdata-par
属性の1つ以上が変更されるたびにコールバックをトリガーします。
- ライブデモを見る→
ここでも、特定の段落要素をターゲットにしています。 変更する属性を選択する選択ドロップダウンに注意してください。 draggable
属性は、オプションで指定しなかったため、修飾されない唯一の属性です。
コードで、 MutationRecord
オブジェクトのattributeName
プロパティを使用して、変更された属性をログに記録していることに注意してください。 そしてもちろん、他のデモと同様に、 MutationObserver
は「開始」ボタンがクリックされるまで変更の監視を開始しません。
ここでもう1つ指摘しておきたいのは、この場合、 attributes
値をtrue
に設定する必要がないということです。 これは、 attributesFilter
がtrueに設定されているために暗示されます。 そのため、私のオプションオブジェクトは次のようになり、同じように機能します。
let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }
一方、 attributeFilter
配列とともにattributes
を明示的にfalse
に設定すると、 false
値が優先され、フィルターオプションが無視されるため、機能しません。
ノードとそのサブツリーの変更を監視する
これまで、各MutationObserver
を設定するときは、ターゲット要素自体と、 childList
の場合は、要素の直接の子のみを処理してきました。 しかし、確かに、次のいずれかの変更を観察したい場合があります。
- 要素とそのすべての子要素。
- 要素とその子要素の1つ以上の属性。
- 要素内のすべてのテキストノード。
上記のすべては、optionsオブジェクトのsubtree
プロパティを使用して実現できます。
サブツリー付きのchildList
まず、直接の子ではない場合でも、要素の子ノードへの変更を探しましょう。 オプションオブジェクトを次のように変更できます。
options = { childList: true, subtree: true }
コード内の他のすべては、いくつかの追加のマークアップとボタンを除いて、前のchildList
の例とほぼ同じです。
- ライブデモを見る→
ここには2つのリストがあり、一方は他方の中にネストされています。 MutationObserver
が開始されると、コールバックはいずれかのリストへの変更をトリガーします。 ただし、 subtree
プロパティをfalse
(存在しない場合のデフォルト)に戻すと、ネストされたリストが変更されたときにコールバックが実行されません。
サブツリーのある属性
別の例を次に示します。今回は、 attributes
とattributeFilter
を持つsubtree
を使用しています。 これにより、ターゲット要素だけでなく、ターゲット要素の子要素の属性の変更を監視できます。
options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
- ライブデモを見る→
これは前の属性デモに似ていますが、今回は2つの異なるselect要素を設定しました。 1つ目は、対象の段落要素の属性を変更し、もう1つは、段落内の子要素の属性を変更します。
繰り返しますが、 subtree
オプションをfalse
に戻す(または削除する)場合、2番目のトグルボタンはMutationObserver
コールバックをトリガーしません。 そしてもちろん、 attributeFilter
を完全に省略でき、 MutationObserver
は、指定された属性ではなく、サブツリー内の属性への変更を探します。
サブツリーのあるcharacterData
以前のcharacterData
デモでは、ターゲットノードが消えてMutationObserver
が機能しなくなるという問題がいくつかあったことを思い出してください。 これを回避する方法はいくつかありますが、テキストノードではなく要素を直接ターゲットにしてから、 subtree
プロパティを使用して、要素がどれほど深くネストされていても、その要素内のすべての文字データをトリガーするように指定する方が簡単です。 MutationObserver
コールバック。
この場合の私のオプションは次のようになります。
options = { characterData: true, subtree: true }
- ライブデモを見る→
オブザーバーを起動した後、CTRL-BおよびCTRL-Iを使用して編集可能なテキストをフォーマットしてみてください。 これは、前のcharacterData
の例よりもはるかに効果的に機能することに気付くでしょう。 この場合、単一のテキストノードではなく、ターゲットノード内のすべてのノードを監視しているため、分割された子ノードはオブザーバーに影響を与えません。
古い値の記録
多くの場合、DOMの変更を監視するときは、古い値をメモして、それらを保存したり、他の場所で使用したりする必要があります。 これは、 options
オブジェクトのいくつかの異なるプロパティを使用して実行できます。
attributeOldValue
まず、変更後の古い属性値をログアウトしてみましょう。 オプションがコールバックとともにどのように表示されるかを次に示します。
options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
- ライブデモを見る→
MutationRecord
オブジェクトのattributeName
プロパティとoldValue
プロパティの使用に注意してください。 テキストフィールドにさまざまな値を入力して、デモを試してください。 保存された以前の値を反映するようにログがどのように更新されるかに注意してください。
characterDataOldValue
同様に、古い文字データをログに記録したい場合、オプションは次のようになります。
options = { characterData: true, subtree: true, characterDataOldValue: true }
- ライブデモを見る→
ログメッセージが前の値を示していることに注意してください。 リッチテキストコマンドを使用してHTMLをミックスに追加すると、状況が少し不安定になります。 その場合の正しい動作はわかりませんが、要素内の唯一のものが単一のテキストノードである場合はより簡単です。
takeRecords()を使用したミューテーションのインターセプト
まだ言及していないMutationObserver
オブジェクトのもう1つのメソッドは、 takeRecords()
です。 このメソッドを使用すると、コールバック関数によって処理される前に検出されたミューテーションを多かれ少なかれインターセプトできます。
この機能は、次のような行を使用して使用できます。
let myRecords = observer.takeRecords();
これにより、指定された変数にDOMの変更のリストが格納されます。 私のデモでは、DOMを変更するボタンがクリックされるとすぐにこのコマンドを実行しています。 開始ボタンと追加/削除ボタンは何もログに記録しないことに注意してください。 これは、前述のように、コールバックによって処理される前にDOMの変更をインターセプトしているためです。
ただし、オブザーバーを停止するイベントリスナーで私が行っていることに注意してください。
btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);
ご覧のとおり、 observer.disconnect()
を使用してオブザーバーを停止した後、インターセプトされたミューテーションレコードにアクセスし、ターゲット要素と記録されたミューテーションのタイプをログに記録しています。 複数のタイプの変更を観察していた場合、保存されたレコードには、それぞれが独自のタイプを持つ複数のアイテムが含まれます。
takeRecords()
を呼び出すことによってこのようにミューテーションレコードがインターセプトされると、通常はコールバック関数に送信されるミューテーションのキューが空になります。 したがって、何らかの理由でこれらのレコードを処理する前にインターセプトする必要がある場合は、 takeRecords()
が便利です。
単一のオブザーバーを使用して複数の変更を監視する
ページ上の2つの異なるノードでミューテーションを探している場合は、同じオブザーバーを使用して検索できることに注意してください。 これは、コンストラクターを呼び出した後、必要な数の要素に対してobserve()
メソッドを実行できることを意味します。
したがって、この行の後:
observer = new MutationObserver(mCallback);
次に、最初の引数として異なる要素を使用して複数のobserve()
呼び出しを行うことができます。
observer.observe(mList, options); observer.observe(mList2, options);
- ライブデモを見る→
オブザーバーを起動してから、両方のリストの追加/削除ボタンを試してください。 ここでの唯一の落とし穴は、「停止」ボタンの1つを押すと、オブザーバーがターゲットにしているリストだけでなく、両方のリストの監視を停止することです。
監視されているノードツリーの移動
最後に指摘するのは、 MutationObserver
は、指定されたノードが親要素から削除された後も、そのノードへの変更を監視し続けるということです。
たとえば、次のデモを試してください。
- ライブデモを見る→
これは、 childList
を使用して、ターゲット要素の子要素への変更を監視する別の例です。 監視されているサブリストを切断するボタンに注意してください。 「スタート…」ボタンをクリックし、「移動…」ボタンをクリックして、ネストされたリストを移動します。 リストが親から削除された後でも、 MutationObserver
は指定された変更を監視し続けます。 これが発生することは大きな驚きではありませんが、覚えておくべきことがあります。
結論
これは、 MutationObserver
のほぼすべての主要機能をカバーしています。 この詳細な説明が、この標準に慣れるために役立つことを願っています。 前述のように、ブラウザのサポートは強力であり、MDNのページでこのAPIの詳細を読むことができます。
この記事のすべてのデモをCodePenコレクションに入れました。デモを簡単にいじりたい場合は、