MutationObserver について理解を深める
onChange
やonClick
やquerySelector
では検出できない変化がある。
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 オブジェクトのtype がattributes のときoldValue プロパティでアクセスできる。このオプションを使うにはattributes 属性をtrue にする必要がある。 |
characterDataOldValue |
boolean |
変更前のテキストの内容を取得できるようになる。MutationRecord オブジェクトのtype がcharacterData のとき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枚目の画像が表示されて、同時に変更を検知したMutationObserver
はMutationRecord
オブジェクトに11-20枚目の画像情報を格納する。再び、画面の一番下までスクロールすると、サイトには21-30枚目の画像が表示されるのだが、このときMutationRecord
オブジェクトに格納されているのは21-30枚目の画像情報だけだ。もう一度一番したまでスクロールすると格納されるのは31-40枚目の画像情報である。このように、MutationRecord
オブジェクトに格納される中身は次々更新され続けていくことに注意してほしい。
以下の表はMutationRecord
が持つプロパティの一部です。
プロパティ | 型 | 説明 |
---|---|---|
type |
String |
ノードのツリーに対する変更の場合は"childList"、 属性値変更の場合は"attributes" 、CharacterData ノードに対する変更の場合は"characterData" を返す |
oldValue |
string or null |
変更前の値をtype の値に応じて返す。attributes の場合、変更前の属性値を返す。characterData の場合、変更前のテキストの内容を返す。type がchildList の場合、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
のふるいはどっちでもいいような気がする。アーリーリターンで処理を止めれるが、どのくらいの負担になるのかは定かではない。
addedNodes
はNodeList
型なのでループをとってNode
型の値を抽出します。欲しいDOMのタグ名、属性値MutationObserver
で使うのに便利なNode
のプロパティは次のようなものです。
nodeName、attributes、id、 textContent classList.contains()、data.match()、
nodeName.toLowerCase()、querySelector()、addedNodes.length()、nodeType
tagNameでなくnodeNameなのは、NodeがElementよりも抽象だからかな
上のやつはGithubで使っている例を見た
nodeTypeを使ってた。
Node
については別のページで解説してあるので詳しくはそちらを参考してください。
-
MutationObserver はDOMの変更をリアルタイム追うためのもので、子要素の増減、属性値の変化、テキストの内容の変化を監視するために使う。それ以外では別のものを使う。マウスイベント、フォームイベント、キーボードイベントなど
-
複数の要素を監視する。(for...of で)
https://chat.openai.com/share/667141ac-44e7-4661-ba16-436020731337
無限ループに陥らない
MutationObserver
のコールバック関数内に自分でDOMを編集するときは無限ループに陥ってしまう可能性がある。変更を検知->コールバック関数内で要素を変更ー>その変更を検知->コールバックで要素を変更ー>その変更を検知→... と無限ループが発生する。これを防ぐために、コールバック関数内でDOMを編集する前に監視を一時停止し、編集が終われば監視を再開するといい。それぞれdisconnect()
、observe()
メソッドを使うといい。
参考サイト
MutationObserver のサンプルコードが書かれた解説記事
Discussion