• MutationObserverはDOMの変更をまとめて受け取って必要な処理だけを動かす

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と必要な値をログで確認してから、実際の更新処理をつなぎます。

戻る