MutationObserverはDOMの変更をまとめて受け取って必要な処理だけを動かす
公開日: 執筆:佐藤
MutationObserverを使うときは、監視するNodeと変更の種類をobserve()で限定します。コールバックへ渡されるMutationRecordを調べ、必要な変更が含まれるときだけ表示や集計を更新します。
監視範囲を文書全体へ広げる前に、子要素の追加と削除、属性の変更、文字データの変更のうち、どれを処理のきっかけにするかを決めてください。
MutationObserverは複数の変更記録をコールバックへ渡す
W3CのDOM4仕様では、MutationObserverのコールバックへMutationRecordのリストとobserver自身が渡されます。DOMの変更はrecord queueへ入り、キューが空でなければ、その内容が一度のコールバックへ渡されます。
一回のコールバックに一件だけ届くとは限りません。最初のrecordだけを見て処理を終えると、同じ回に含まれた追加や削除を見落とします。受け取ったrecordsを順に調べるか、目的の変更が一件でも見つかったら一回だけ更新する形にします。
MutationRecordの主な値は次のように使い分けます。
| 値 | 確認できること |
|---|---|
type |
childList、attributes、characterDataのどの変更か |
target |
変更を受けたNode |
addedNodes |
追加されたNode |
removedNodes |
削除されたNode |
attributeName |
変更された属性名 |
oldValue |
設定によって記録された変更前の値 |
すべての値を毎回読む必要はありません。まずtypeで処理を分け、その処理に必要な値だけを読みます。
observe()では対象Nodeと変更の種類を指定する
observe(target, options)のtargetには監視するNodeを渡します。optionsでは、受け取りたい変更を指定します。
| option | trueで監視する変更 |
|---|---|
childList |
target直下の子Nodeの追加と削除 |
attributes |
targetの属性変更 |
characterData |
targetとなる文字データの変更 |
subtree |
targetだけでなく、その子孫も監視範囲に含める |
childList、attributes、characterDataのうち、目的に合うものを少なくとも一つ有効にします。どれも有効にしなければ、対応する変更記録はコールバックへ届きません。
subtree: trueだけでは、変更の種類を指定したことになりません。子孫の子Nodeを監視するなら、childList: trueと組み合わせます。子孫の属性を監視するなら、attributes: trueも必要です。
子要素が変わったときだけ件数を更新する
次は、#notices内の項目数を#notice-countへ表示する説明用の仮例です。実在するサイトのコードではありません。
(function () {
var list = document.getElementById('notices');
var count = document.getElementById('notice-count');
var stop = document.getElementById('stop-notice-watch');
if (!list || !count || typeof window.MutationObserver !== 'function') {
return;
}
function updateCount() {
count.innerHTML = list.getElementsByTagName('li').length;
}
var observer = new MutationObserver(function (records) {
var needsUpdate = false;
var i;
for (i = 0; i < records.length; i += 1) {
if (
records[i].type === 'childList' &&
(records[i].addedNodes.length || records[i].removedNodes.length)
) {
needsUpdate = true;
break;
}
}
if (needsUpdate) {
updateCount();
}
});
observer.observe(list, {
childList: true
});
updateCount();
if (stop) {
stop.addEventListener('click', function () {
observer.disconnect();
}, false);
}
}());
この例は#notices直下の追加と削除だけを監視します。属性や子孫全体は監視しません。recordsが複数あっても、必要な変更が見つかった回のupdateCount()は一回だけです。
対象のブラウザーでMutationObserverを利用できるかは、実装前に確認します。利用できない場合の動作も決め、監視がないだけで主要な内容を読めなくなる設計は避けます。
属性を監視するなら対象属性を絞る
属性変更が必要な場合は、attributes: trueを指定します。特定の属性だけを扱うなら、attributeFilterへ属性名を指定できます。
observer.observe(button, {
attributes: true,
attributeFilter: ['aria-expanded']
});
コールバックでは、record.type === 'attributes'を確認してからrecord.attributeNameを読みます。変更前の値が必要なら、attributeOldValue: trueも指定します。
文書全体へsubtree: trueとattributes: trueを指定すると、目的と関係のない属性変更も監視対象へ入ります。処理したい部品に近いNodeと属性名へ絞った方が、どの変更で処理が動いたかを追えます。
disconnect()は不要になった監視と未通知の記録を外す
監視が不要になったらdisconnect()を呼びます。DOM4仕様では、observerを登録したNodeから監視を外し、record queueも空にします。再びobserve()を呼ぶまで、コールバックは呼ばれません。
画面から部品を外す処理、別画面へ切り替える処理、監視を一時停止する操作にdisconnect()を対応させます。監視開始のコードだけを残すと、部品を使わなくなったあとも変更を受け取る設計になります。
キューに残った記録を処理してから停止する必要がある場合は、先にtakeRecords()で記録を取り出す方法があります。単に監視を終える場合は、未通知の記録を捨てることを理解したうえでdisconnect()を使います。
最初に、処理のきっかけにするDOM変更を一つ選んでください。その変更が起こる最も狭いNodeへobserve()を設定し、MutationRecord.typeと必要な値をログで確認してから、実際の更新処理をつなぎます。
関連記事

