「ページを切り替えなくても値の更新をする方法を知りたい」
「特定の要素が選択されたときだけのCSSを書くことをやりたい」
「ReactのようなDOMの監視という意味がイマイチわからず使っている」
本日はそんな方に向けてMutationObserverというものを使ってWebページのDOM監視を体験してみたいと思います。
実務での登場頻度は少ないかもしれませんが、最近トレンドになっているReactのようなモダンJSが何をやっているかが理解できる内容となっています。
ウェブ開発において動的なコンテンツの変更を検知し、ページ移動なしで対応することは重要なスキルです。
JavaScriptにはこのような機能を提供する便利なツールがあって、MutationObserver(ミューテーションオブザーバー)はDOMの変更を監視し、リアルタイムで対応するのに役立ちます。
この記事では、MutationObserverの基礎的な使い方からコアな機能までを解説します。
DOMの変更がどのように検知され、それに対してどのように反応するか、そしてどのように利用するかを理解することで、より柔軟かつ効率的なウェブアプリケーションを構築する手助けになることでしょう。
また動画もあるので必要に応じて使ってください。
JavaScriptのMutationObserverの使い方
MutationObserverはHTMLのDOM監視をするためのクラスです。
JavaScriptに標準で搭載されているのでインストール不要で、インスタンス化するだけで使用できます。
基本的な使い方としては以下のように関数型にして引数にDOMの変更履歴が保存されるようになります。
const mb = new MutationObserver((entries) => {
console.log(entries);
});
DOMの変更履歴とは何かを知るために以下のようなHTMLを用意して実験してみます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body>
<div id="cards" class="cards">
<div class="card" contenteditable="true">タスクA</div>
<div class="card taskB" contenteditable="true">タスクB</div>
<div class="card" contenteditable="true">タスクC</div>
</div>
</body>
</html>
.cards {
display: flex;
flex-direction: column;
gap: 8px;
}
.card {
width: 150px;
height: 150px;
background: #ccc;
font-weight: bold;
font-size: 24px;
padding: 24px;
}
コンソールには以下のような配列が表示されるはずです。
要素は何種類かあってコレだけ見てもわからないと思いますが、コレらが先ほど言ったDOMの変更履歴になります。
実務では監視したい要素があるはずなので、インスタンス化することで使用できるobserveメソッドというものの第一引数にHTML要素を指定します。
また第二引数にchildList: trueとすることで第一引数で指定した要素の子要素を監視するという意味になります。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// observeメソッドで監視する対象を指定(cardsの子要素を指定した)
mb.observe(cards, { childList: true });
例えばcardsの子要素を一つ削除することをJavaScript側でやってみます。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
mb.observe(cards, { childList: true });
// ここを追加
cards.children[0].remove();
タスクAと書かれたカードが削除されましたね。
ここで再度コンソールを見てみます。
removedNodesというプロパティに[div.card]と書かれていています、こちらがcardというclass名がついたdivタグが削除されたことを意味しています。
つまりcardsの子要素が1件削除されたことが記録されたわけです。
要素自体だけでなく属性名の変更も追跡することが可能です。
JavaScriptを以下のように書き換えるとcardsの属性を監視するようになります。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// ここから変更
mb.observe(cards, { attributes: true });
cards.classList.add('active');
cards.classList.remove('active');
上記のコードではclass=”cards active”にしてからclass=”cards”に戻すことをしています。
class名の変更を2回行ったわけですが、コンソールに行くと配列の要素が2個に増えていますね。
こちらから分かるように「1変更ごと」に保存していく仕組みになっているのがMutationObserverのすごい点です。
しかし現状のものだと「変更の回数」は分かるのですが、特に削除されたものが何だったのかまでは確認することができないような仕様です。
JavaScriptを以下のように変更することで、変更したかだけでなく「何が削除されたか」も確認できるようになります。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// ここを修正
mb.observe(cards, { attributes: true, attributeOldValue: true });
cards.classList.add('active');
cards.classList.remove('active');
コンソールに表示される配列の1番目のものが以下のようになっています。
oldValueというプロパティにcards activeと書かれていて、過去にこのようなclass名にあったことが確認できるわけです。
ちなみに属性名といっても種類はたくさんあるので「idの変更を知りたい」ということであれば以下のように書くこともできます。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
mb.observe(cards, {
attributes: true,
attributeOldValue: true,
// ここを追加
attributeFilter: ['id'],
});
cards.classList.add('active');
cards.classList.remove('active');
// ここを追加
cards.id = 'test';
observeメソッドの第二引数でattributeFilter: [‘id’]としたことでidの変更を見るようになります。
さらにcards.id = ‘test’として新しくidの値を変更しました。
そうするとコンソールには以下のようになっています。
attributeNameというプロパティにidが記載されていて、oldValueにはcardsが記載されていて古いid名のことですね。
また要素の数、つまり変更の回数は1回に変わっていることも興味深いです。
idの変更と一緒にclassの変更もしていたはずですが、そちらは監視対象から外れたことを意味しています。
このように網羅的に変化を保存することもできるし、特定の内容だけに絞って監視することもできるわけです。
属性名だけでなくテキストについても同じようなことができます。
JavaScriptのコードを以下のように作り直してみます、taskBというclass名のものを監視対象にするものです。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// ここを変更
const taskB = document.querySelector('.taskB');
mb.observe(taskB.childNodes[0], {
characterData: true,
characterDataOldValue: true,
});
テキストの変更はHTML側で事前にcontenteditable=”true”を記載していましたので、ブラウザ上でダブルクリックするとテキストの書き換えが可能です。
そうすると1文字消した回数分だけ変更履歴がコンソールに表示されるのがわかります。
oldValueには1個前のテキストの状態が記載されていて、最新のテキストはtargetプロパティの中にあるdataという場所に記載されています。
ちなみにobserveメソッドの第一引数で要素の指定をするのですが、親要素から指定してもOKです。
インデックス番号は動的に変わるので注意しましょう。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// コレでもOK
mb.observe(cards.children[0].childNodes[0], {
characterData: true,
characterDataOldValue: true,
});
また今回のような簡単なHTMLではなく実務では子要素が大量にある場合があり、その際にインデックス番号で指定したり個別に要素の取得をするのは骨が折れます。
そこでobserbeメソッドの第一引数で親要素を指定して、第二引数でsubtree: trueとすることで子要素を全て監視するようにすることも可能です。
const cards = document.querySelector('.cards');
const mb = new MutationObserver((entries) => {
console.log(entries);
});
// コレなら子要素を全て監視対象にできる
mb.observe(cards, {
characterData: true,
characterDataOldValue: true,
subtree: true,
});