はじめに
コードを書いている際 「MutationObserver」 に助けられたことがありました。
実際に使用するに至った背景は最後の方でおまけとして語っていますので、もしもご興味があれば。
ということで復習も兼ねて個人的にまとめてみたいと思います。
あくまで理解に努める形なので、もっと深い部分まで知りたい方はMDNを見ていただければと思います。
MutationObserver-MDN
※誤りありましたら、ご指摘いただけますと幸いです。
MutationObserverとは
まずそもそも、MutationObserver とは。
MDNにはこう書いてあります。
MutationObserver インターフェイスは、 DOM ツリーへ変更が加えられたことを監視することができる機能を提供します。
めっちゃ簡単に言うと、DOMの変更をリアルタイムで検出できるJavaScriptのAPIと言ったところでしょうか。
例えば、ノード(要素)が追加されたり、属性・テキストが変更されたりというような変更の検知が可能となります。
DOMContentLoadedとの違い
また後で詳しく説明しますが、実際に私がMutationObserverを使用するに至った背景なんですが、イベントリスナーとしてDOMContentLoadedを使用しても、その後に走るレンダリング処理によって、思うような画面描写ができなかったことが理由としてありました。
では一体、DomContentLoadedとは何が違うのか。
DOMContentLoadedの発火タイミングは、 HTML の解析が完了し、最初のDOMツリーが構築されたときに発火します。
よってスクリプトが読み込まれた時点でのDOMの状態を対象とするため、それ以降に追加される要素には影響しません。
一方でMutationObserverについては、DOMContentLoadedの後でも、動的に追加・変更されたDOM要素を検知することが可能です。
よってDOMContentLoaded後に追加・削除された要素や属性の変更も追跡可能となっているわけです。
使い方
実際の使い方を見てみましょう。
今回はそこまでコード量が多くないので、JSもHTMLファイル内にscriptタグで記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MutationObserverのデモ</title>
</head>
<body>
<div id="target">監視対象の要素</div>
<button onclick="changeText()">テキストリセット</button>
<button onclick="addElement()">要素追加</button>
<script>
const targetNode = document.getElementById('target');
// MutationObserverの作成
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
console.log("変更が検知されました:", mutation);
});
});
// 監視の設定
const config = { childList: true, subtree: false };
observer.observe(targetNode, config);
// テキストを変更する関数
function changeText() {
targetNode.textContent = "テキストをリセットしました!";
}
// 新しい要素を追加する関数
function addElement() {
const newElement = document.createElement("p");
newElement.textContent = "追加された要素";
targetNode.appendChild(newElement);
}
</script>
</body>
</html>
実際に起動すると、
要素追加ボタンで「追加された要素」という文字が追加され、
リセットボタンで追加された要素が削除されます。
どちらかというと今回この動きはあまり重要ではなくて、コンソール上に出力されているログに注目していきます。
紐解いていく
さて、上記のコードでは、「監視対象の要素」とあるdiv要素を監視しています。
監視を開始するにあたって、まずは下記のようにMutationObserverを作成する必要があります。
// MutationObserverの作成
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
console.log("変更が検知されました:", mutation);
});
});
何やらインスタンス生成時に「mutationsList」なるものが渡されていますね、、
その先のログ出力で気づいた方が多いとは思いますが、mutationsList は、監視対象のDOMに変更が発生したときに、どんな変更が起こったのかを詳細に記録した配列になります。
ちなみにmutationには何が格納されているかコンソールで確認してみましょう。
何やら MutationRecord なるオブジェクトが格納されているようです。
このMutationRecordにはDOMの変更に関する情報が格納されており、プロパティにはtype(変更の種類)、target(変更が発生した要素)、attributeName(変更された属性名)なんかがあります。
コードに戻ります。
// 監視の設定
const config = { childList: true, subtree: true };
observer.observe(targetNode, config);
このconfigでは MutationObserver の監視対象の設定を行っています。
今回は childList(子ノードの追加・削除を監視)、subtree(監視対象の子要素も含める)を設定しています。booleanで監視対象とするか否かを設定できます。
ちなみに、attributes(属性の変更を監視)、characterData(テキストの変更を監視)なんかもあります。
そして、その次の行で監視を開始している形です。
第一引数には監視対象、第二引数に先程の監視設定を追加しています。
動きを見てみる
さて、では実際に動かしてみます。
まず、要素追加ボタンを押下して対象となっているdiv要素に文字列が追加されると、コンソールに変更が検知された旨のメッセージが表示されます。
ここで思い出してほしいのが、targetNodeで監視対象となっていたdiv要素、そして、configで設定した childList: trueです。
今回設定対象のdiv要素に対して、addElement関数内、appendChildで子要素が追加された、という変更を、MutationObserverが検知したことでコンソール出力がされた形になります。
追加される度にコンソール出力されますし、テキストリセットボタンで文字列が置き換わってもコンソール出力されます。
一方で、configで設定した subtree は一体何者なのでしょうか。
少しコードを変更して動きを見てみようと思います。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MutationObserverのデモ</title>
</head>
<body>
<div id="target">監視対象の要素
<div id="target">監視対象の子要素</div>
</div>
<button onclick="changeText()">テキストリセット</button>
<button onclick="addElement()">要素追加</button>
<button onclick="addChildElement()">孫要素追加</button>
<script>
const targetNode = document.getElementById('target');
const targetChildrenNode = document.getElementById('targetChildren');
// MutationObserverの作成
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
console.log("変更が検知されました:", mutation);
});
});
// 監視の設定
const config = { childList: true, subtree: false };
observer.observe(targetNode, config);
// テキストを変更する関数
function changeText() {
targetNode.textContent = "テキストをリセットしました!";
}
// 新しい要素を追加する関数
function addElement() {
const newElement = document.createElement("p");
newElement.textContent = "追加された要素";
targetNode.appendChild(newElement);
}
// 孫要素に新しい要素を追加する関数
function addChildElement() {
const newElement = document.createElement("p");
newElement.textContent = "追加された孫要素";
targetChildrenNode.appendChild(newElement);
}
</script>
</body>
</html>
上記では新たに監視対象のdiv要素に、子要素を追加し、この子要素にテキストを追加するボタンを用意します。
つまり、監視対象のdiv要素に孫要素が追加されるような形になります。
さてこの時、 config は変わらず childList と subtree に true を設定して孫要素追加ボタン押下時のコンソールを確認してみましょう。
孫要素追加ボタンを押下すると、変わらずコンソールが出力されました。
続いて、subtreeを false に設定してみます。
孫要素追加ボタン押下時のコンソール出力を見てみると、
なにも出力されなくなりました。
これは、childListが監視するのは子要素までなのに対し、subtreeは孫要素も監視しており、
subtreeにfalseを設定したことで、監視対象から外れたというわけです。
おまけ:なぜ使用するに至ったのか
参考までに、私が今回MutationObserverを使用するに至った背景ですが、前半でも軽く説明した通り、イベントリスナーとしてDOMContentLoadedを使用しても、その後に走るレンダリング処理によって、思うような画面描写ができなかったことが理由としてありました。
DOMの構築が完了した後で要素の変更をしたいのに、、と悩んでいたんですが、この原因は恐らくReactの仮想DOmレンダリングによるものだと思ってます。
DOMContentLoaded はReactの仮想DOMのレンダリングを待てないのに対して、MutationObserverは、Reactが仮想DOMを変更するたびに、実際のDOMの変化を検知することができます。
この検証に際して、React Hooks についてもまた学んだので、また次の機会にでもまとめてみようと思います。
最後に
MutationObserverを使うにも注意が必要です。
監視対象を広げすぎるとパフォーマンスに影響してしまいます。
必要な特定の要素のみを監視対象にする形が理想でしょう。
さて、初めてのフロントエンド開発はつまずくことも多いですが、焦らず一つずつ原因を追求して、理解を深めてみようと思います。
引き続き頑張ります。