MutationObserver の使い方

DOM の変化を監視して、変化が発生すればコールバック関数を呼び出すことができる MutationObserver の使い方に関する解説のような覚書です。observe() メソッドの詳細や MutationObserver の使用例などを掲載しています。

作成日:2022年12月24日

MutationObserver とは

MutationObserver は DOM の変化(Mutation)を監視(Observe)し、DOM に変化があった場合にコールバック関数を呼び出す組み込みオブジェクト(Built-in Object)です。

MutationObserver を使用すると、指定された DOM(ノード)を監視して DOM が動的に変更された際に、その変化に応じて何らかの処理を実行する(コールバック関数を呼び出す)ことができます。

現時点では主要なブラウザのほどんどで MutationObserver を利用することができます。

MutationObserver 関連リンク

参考サイト

その他の Observer のページ

MutationObserver の使い方

大まかな使い方は以下になります。

1. コンストラクターでインスタンス(オブザーバー)を生成
  • コンストラクター MutationObserver() を使って MutationObserver のインスタンスを生成します。
  • コンストラクターには、DOM の変更(変化)を検知した際に実行するコールバック関数を渡します。
2. observe() メソッドを実行して DOM の変化を監視
  • 生成したインスタンスのメソッド observe() を実行して DOM の変化を監視します。
  • observe() の引数には監視対象の DOM とどのような変更を検知するのかを指定します。

生成した MutationObserver のインスタンス(オブザーバー)の observe() メソッドを実行すると、オブザーバーは監視対象の DOM を監視し、変更を検知するとコールバック関数を実行します。監視を停止するには disconnect() メソッドを実行します。

以下は body 及びその子孫ノードを監視して変更が発生したら、コールバック関数でその変更の内容をコンソールに出力する例です。

コールバック関数には変更を検知したら実行する処理を記述します。

コールバック関数の第一引数には発生した変化の内容を表す MutationRecord オブジェクトの配列が渡されるので、この例では forEach() で各 MutationRecord(変化の内容)を出力しています。

コンストラクター MutationObserver() には、コールバック関数を渡して MutationObserver のインスタンス(オブザーバー)を生成します。

observe() メソッドの第一引数には監視対象の DOM ノードを、第二引数には監視のオプションオブジェクトを指定します。この例では監視対象として body を指定し、第二引数には全ての変更を監視するように childList、attributes、characterData に true を指定しています。

また、body 及びその子孫ノードを監視するように subtree: true も指定しています。

//コールバック関数
const callback = (mutations) => {
  //第一引数 mutations は変化の内容を表す MutationRecord オブジェクトの配列
  mutations.forEach((mutation) => {
    //各 MutationRecord オブジェクトを出力
    console.log(mutation);
  })
}

//コンストラクターにコールバック関数を渡してオブザーバーを生成
const observer = new MutationObserver(callback);

//監視対象の DOM(ノードや要素)この場合は body を監視
const target = document.body;

//監視のオプション
const config = {
  childList: true, //対象ノードの子ノードに対する追加・削除の監視を有効に
  attributes: true, //対象ノードの属性に対する変更の監視を有効に
  characterData: true, //対象ノードのテキストデータの変更の監視を有効に
  subtree: true, //対象ノードとその子孫ノードに対する変更の監視を有効に
};

//observe() メソッドに監視対象と監視オプションを指定して実行(監視を開始)
observer.observe(target, config);

例えば、以下のような HTML で </body> の直前に上記を script タグに記述すると

<!DOCTYPE html>
<html lang="ja">
<!-- 中略 -->
<body>
  <main>
    <h1>Mutation Observer</h1>
    <p>Lorem ipsum dolor sit amet.</p>
  </main>
  <button>Change</button>
<script>
  //上記の記述(省略)
</script>
</body>
</html>

この例の場合、以下のように表示されます。

変更を何もしていないのに、開発ツールのコンソールタブで確認すると2つの MutationRecord オブジェクトが出力されていますが、これは body を監視対象にしているため、</body> タグの前後の改行を変更と検知してしまうからです(タイミングの問題もあります)。

※ body より下の階層の要素を監視対象にする場合は、このようなことは発生しません。

出力された MutationRecord オブジェクトは左側の ▶ をクリックして展開すると、プロパティを確認できます。最初の MutationRecord を展開して、更に addedNodeds を展開すると data に "\n\n" とあり、改行が追加されたと検知されているのがわかります。

紛らわしいので、HTML の </body> タグの前後の改行を削除して以下のように記述するか、

</script></body></html>

または、以下のように DOMContentLoaded イベントを利用すれば、不要な出力は消えます。

document.addEventListener('DOMContentLoaded', ()=> {
  //コールバック関数
  const callback = (mutations) => {
    mutations.forEach((mutation) => {
      console.log(mutation);
    })
  }
  //オブザーバーを生成
  const observer = new MutationObserver(callback);
  //監視対象
  const target = document.body;
  //監視オプション
  const config = {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
  };
  //監視を開始
  observer.observe(target, config);
})

以下を JavaScript に追加して、ボタンをクリックしたら5つの変更を行うようにます。

//ボタン要素
const button = document.querySelector('button');
//ボタンにクリックイベントのリスナーを設定
button.addEventListener('click', ()=> {
  // body の背景色を変更
  document.body.style.backgroundColor = 'yellow';
  // h1 要素のテキストを変更
  document.querySelector('h1').textContent ='Changed Result';
  // main 要素に p 要素を追加
  const foo = document.createElement('p');
  foo.textContent = 'this is foo!';
  document.querySelector('main').appendChild(foo);
  // ボタン要素に disabled 属性を追加
  button.setAttribute('disabled', true);
  // ボタン要素のテキストデータを変更
  button.firstChild.data = 'Changed!'
});

ボタンをクリックすると、上記により DOM への変更が行われ、コンソールには以下のようにそれぞれの変更に対応する5つの MutationRecord オブジェクトが出力されます。

変更の内容を表す MutationRecord の type には変更の種類(監視オプションで指定した childList、attributes、characterData に対応)、target には対象ノードがセットされます。

例えば、3つ目の MutationRecord を展開して、addedNodes を更に展開すると最初の要素 0: に p とあり、p 要素が1つ追加されたことがわかります。また、target の対象ノードは main 要素であり、変更の種類は childList であることが確認できます。

もし main 要素に対しての要素の追加の変更のみを検知したい場合は、observe() メソッドに以下のように対象に main 要素、オプションに childList: true を指定すれば良いことになります(以下のように指定した場合は、他の4つの変更は検知できなくなります)。

const target = document.querySelector('main');
const config = {
  childList: true,
};
observer.observe(target, config);

コールバック関数では、第一引数に MutationRecord オブジェクトの配列を受け取るので forEach() を使って個々の MutationRecord を調べることができます。

この例では単に MutationRecord を出力しているだけですが、以下は p 要素が追加された場合は、そのテキストが空でなければ、inserted というクラスを追加し、そのテキストをコンソールに出力するコールバックの例です。

MutationRecord のプロパティにセットされる値は変更の種類(type)によって異なるので、値が有効かどうかなど(null でないか等)を確認して必要な処理を記述します。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    // 変更の種類が childList であれば(もし childList のみを監視していれば、この判定は不要)
    if ( mutation.type === 'childList' ) {
      // addedNodes[0] が存在し、そのタグ名が P であれば
      if(mutation.addedNodes[0] && mutation.addedNodes[0].tagName ==='P') {
        // addedNodes[0] の子ノードの data プロパティが空でなければ
        if(mutation.addedNodes[0].firstChild.data){
          // その p 要素にクラス属性を追加
          mutation.addedNodes[0].setAttribute('class', 'inserted');
          // その p 要素のテキストをコンソールに出力
          console.log(mutation.addedNodes[0].firstChild.data);
        }
      }
    }
    // その他の処理・・・
  })
}

何を監視対象にしてどのオプションを指定するかは、例えば、body や document などの上の階層のノードを監視対象にして、subtree: true を含む全ての監視オプションを有効にし、変更を発生させてコールバック関数で MutationRecord の内容(type や target など)を確認すれば判断できます。

MutationObserver のサンプル

以下は MutationObserver を使って、動的に追加された img 要素の height と width を調べて、横長の場合は landscape、縦長の場合は portrait というクラスをその img 要素に自動的に追加する例です。

この例ではボタンをクリックすると id="photos" の div 要素に画像を動的に追加します(※ この場合、MutationObserver を使わずに、画像を追加する際に縦横比を調べてクラスを追加することもできます)。

HTML
<div id="photos"></div>
<button id="addImage">Add Image</button>

DOM の変更を検知した際に実行するコールバック関数を定義(2〜23行目)し、MutationObserver() コンストラクターに渡して MutationObserver のインスタンスを生成しています(26行目)。

コンストラクターに渡すコールバック関数は第一引数に DOM の変化の内容を表す MutationRecord オブジェクトの配列を受け取るので、forEach() を使って個々の MutationRecord を調べます。

MutationRecord の addedNodes プロパティには、DOM に追加されたノードの NodeList(配列のようなオブジェクト)がセットされています。

addedNodes プロパティ(にセットされた NodeList)に1つ以上のノードが入っていれば、forEach() を使って個々のノードを調べます(6行目の判定は省略可能です)。

nodeType が1で(ノードが要素の場合)、タグ名が IMG であれば追加されたノードは img 要素なので、画像の load イベントを使って読み込みが完了した時点で縦横比を調べてクラスを追加します(nodeType の判定部分は省略可能です。タグ名は大文字です)。

26行目では、定義したコールバック関数をコンストラクターに渡して、生成した MutationObserver のインスタンスを変数 mo に代入しています。

DOM の監視を開始する observe() メソッドの第一引数には監視対象となる DOM を指定し、第二引数には監視のオプション(どのような変更を検知するか)を指定します。

この例では、id="photos" の div 要素に画像が追加された場合に処理を実行するので、監視対象のノードを id="photos" の div 要素とします。

監視のオプションでは、childList: true を指定して対象ノード(id="photos" の div 要素)の子ノードに対する追加・削除を監視するようにし、また、この例では対象ノードとその子孫ノードも監視するように subtree: true を指定しています。

observe() メソッドに監視対象の DOM と監視のオプションを指定して実行すると、監視が開始され、div#photos に img 要素が追加されるとコールバック関数が実行されます。

JavaScript
// コールバック関数の定義
const addImageClass = (mutations) => {
  //引数の mutations は DOM の変化の内容を表す MutationRecord オブジェクトの配列
  mutations.forEach( mutation => {
    // MutationRecord オブジェクトの addedNodes プロパティの長さが 1 以上であれば
    if (mutation.addedNodes.length >= 1) {
      //addedNodes(追加されたノードの NodeList)の個々のノードを調べる
      mutation.addedNodes.forEach( addedNode => {
        //追加されたノード(addedNode)が img 要素であれば
        if(addedNode.nodeType === 1 && addedNode.tagName==='IMG') {
          //画像の load イベントで読み込みが完了した時点でクラスを追加
          addedNode.addEventListener('load', () => {
            let imgClass = 'landscape';
            if(addedNode.height > addedNode.width) {
              imgClass = 'portrait'
            }
            addedNode.classList.add(imgClass)
          });
        }
      });
    }
  });
}

// コンストラクターにコールバック関数を渡してインスタンスを生成
const mo = new MutationObserver(addImageClass);

//監視対象の DOM(ノード)
const moTarget = document.getElementById('photos');

// 監視のオプション
const moOption = {
  childList: true, //対象ノードの子ノードに対する追加・削除を監視
  subtree: true  //対象ノードとその子孫ノードも監視
};

// 監視対象とオプションを指定して observe() メソッドを実行(監視を開始)
mo.observe(moTarget, moOption);

/* 以下はボタンをクリックして画像を動的に追加する処理  */

//画像を追加するボタン
const btn = document.getElementById('addImage');

//ボタンをクリックして画像を追加
btn.addEventListener('click', () => {
  const img = document.createElement('img');
  img.src = 'images/photo1.jpg';
  moTarget.append(img);
});

この例では、observe() に指定するオプションで subtree: true としているので、例えば div#photos の下に新たに div#nature を作成して、以下のようにそこへ画像を追加する場合もコールバック関数が呼び出されます。

btn.addEventListener('click', () => {
  const img = document.createElement('img');
  img.src = 'images/photo1.jpg';
  //div#nature へ追加
  document.getElementById('nature').append(img);
});

但し、オプションで subtree: true を指定していない場合は、div#photos の子孫ノードは監視されないので、コールバック関数は呼び出されません

また、以下のように img 要素を p 要素でラップして追加した場合は、コールバック関数は呼び出されません。これはコールバック関数の定義(10行目)で、追加されたノードが img 要素かどうかを判定しているためです。

btn.addEventListener('click', () => {
  const img = document.createElement('img');
  img.src = 'images/photo1.jpg';
  const p = document.createElement('p');
  //img 要素を p 要素でラップして追加
  p.append(img);
  moTarget.append(p);
});

img 要素が何らかの要素でラップされて追加された場合にも対応するには、以下のように追加されたノードの子孫を調べて img 要素があれば処理を適用するようにできます。

この場合、追加されたノードを基点に querySelectorAll() を実行しますが、テキストノードなどは querySelectorAll() をプロパティに持っていないので、ノードが要素であるかを確認しています。

const addImageClass = (mutations) => {
  mutations.forEach( mutation => {
    if (mutation.addedNodes.length >= 1) {
      mutation.addedNodes.forEach( addedNode => {
        //ノードが要素であれば
        if(addedNode.nodeType === 1){
          //ノードの子要素から img 要素を取得
          const imgs = addedNode.querySelectorAll('img');
          //ノードの子要素に img 要素があれば
          if(imgs.length >=1 ) {
            imgs.forEach( img => {
              img.addEventListener('load', () => {
                let imgClass = 'landscape';
                if(img.height > img.width) {
                  imgClass = 'portrait'
                }
                img.classList.add(imgClass)
              });
            })
          }
          //ノードが img 要素の場合
          if(addedNode.tagName==='IMG') {
            addedNode.addEventListener('load', () => {
              let imgClass = 'landscape';
              if(addedNode.height > addedNode.width) {
                imgClass = 'portrait'
              }
              addedNode.classList.add(imgClass)
            });
          }
        }
      });
    }
  });
}

画像を追加する際に縦横比を調べてクラスを追加する

本題と外れますが、上記の例の場合、MutationObserver を使わなくても、画像を追加する際に縦横比を調べてクラスを追加することができます。

画像の読み込みには、load イベントや Promise などを利用することができます。

btn.addEventListener('click', () => {
  const img = document.createElement('img');
  img.src = 'images/photo1.jpg';
  img.onload = () =>{
    let imgClass = 'landscape';
    if(img.height > img.width) {
      imgClass = 'portrait'
    }
    img.classList.add(imgClass)
    moTarget.append(img);
  }
});
btn.addEventListener('click', () => {
  new Promise((resolve, reject) => {
    const img = document.createElement('img');
    img.src = 'images/photo1.jpg';
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Error: Image Not Found "${img.src}".`));
  }).then( (img) => {
    let imgClass = 'landscape';
    if(img.height > img.width) {
      imgClass = 'portrait'
    }
    img.classList.add(imgClass)
    moTarget.append(img);
  }).catch(
    (error) => console.log(error.message)
  );
});

コンストラクター MutationObserver()

new 演算子を使用してコンストラクター MutationObserver() にコールバック関数を渡すと、DOM の変化を検知した際にコールバック関数を実行する MutationObserver のインスタンスを作成して返します。

コンストラクターの構文
const observer = new MutationObserver(callback);

コンストラクターの引数に、直接コールバック関数を記述することもできます。

const observer = new MutationObserver((mutations, observer) => {
  //コールバック関数の処理
});

引数(callback)

対象となる DOM の変更が発生するたびに呼び出されるコールバック関数

戻り値(observer)

MutationObserver のインスタンス(オブザーバー)。

オブザーバー(MutationObserver インスタンス)

MutationObserver() コンストラクターで生成された MutationObserver のインスタンスをオブザーバーと呼びます。MutationObserver のインスタンスでは以下のメソッドを利用できます。

メソッド 説明
observe() 監視を開始し、指定された変更を検知するとコールバック関数を呼び出します。
disconnect() observe() が再び呼び出されるまで監視を停止します。
takeRecords() コールバック関数で処理されていない DOM の変更に一致するすべてのリスト(配列)を返し、変更キューを空にします。

コンストラクターは MutationObserver のインスタンスを返すので、MutationObserver インスタンスのメソッドをチェインして記述することができます。

以下は、コールバック関数やインスタンス、observe() の引数を別途変数に定義して記述しています。

//コールバック関数
const callback = (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
  })
}
//インスタンスを生成
const observer = new MutationObserver(callback);

//監視対象のノード
const target = document.getElementById('foo');

//監視のオプション
const config = {
  childList: true,
  subtree: true,
};

//監視を開始
observer.observe(target, config);

上記は以下のように、コールバック関数やオプションを引数に直接指定して記述することもできます。

//インスタンスを生成
const observer = new MutationObserver( (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
  })
});
//監視を開始
observer.observe( document.getElementById('foo'), {
  childList: true,
  subtree: true,
});

コンストラクターの戻り値に observe() をチェインして以下のように記述することもできます。

//インスタンスを生成して監視を開始
const observer = new MutationObserver( (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
  })
}).observe( document.getElementById('foo'), {
  childList: true,
  subtree: true,
});

observe() メソッド

生成した MutationObserver のインスタンス(オブザーバー)で observe() メソッドを実行すると、DOM の変化の監視を開始し、指定された変更を検知するとコールバック関数を呼び出します。

disconnect() メソッドで停止するまで監視は続きます。停止後、再度 observe() メソッドを呼び出してオブザーバーを再利用することができます。

observe() メソッドを呼び出すには、第一引数(target)に監視対象となる DOM ノードを指定し、第二引数(options)にどのような変更を検知するかを定義したオブジェクトを指定します。

構文
mutationObserver.observe(target, options)
引数 説明
target DOM ツリー内で変更を監視する対象の DOM(ノードや要素)
options どのような変更を検知するかを真偽値で指定するオプションオブジェクト

第一引数(target)に指定したノードのみが監視されますが、第二引数(options)の subtree: true を指定することでその子孫ノードも監視対象にすることができます。

第二引数(options)には以下のようなどのような変更を検知するかを真偽値で指定するプロパティで構成されたオプションオブジェクトを指定します。

※ 任意のプロパティを指定できます(複数指定可能)が、少なくとも childList、attributes、characterData のいずれか1つは指定する必要があります(これらは MutationRecord の type プロパティの値に対応しています)。

プロパティ 説明
childList 対象ノードの子ノード(テキストノードやコメントノードを含む)に対する変更(追加・削除)の監視を有効にする。デフォルトは false
attributes 対象ノードの属性に対する変更の監視を有効にする。デフォルトは false。※ attributeOldValue:true または attributeFilter を指定する場合は true になるので attributes:true の指定を省略できます
characterData 対象ノードのテキストデータ(#text の data)の変更の監視を有効にする。デフォルトは false。※ characterDataOldValue: true を指定する場合は true になるので characterData: true の指定を省略できます
subtree 対象ノードとその子孫ノードに対する変更の監視を有効にする。デフォルトは false
attributeFilter attributes:true を指定した場合、デフォルトではすべての属性が対象になりますが、このプロパティに監視する特定の属性名の配列を指定して、監視する対象の属性を限定することができます。但し、attributeFilter:[] と空の配列を指定すると、どの属性も監視しません。
attributeOldValue 対象ノードの変更前の属性値を記録する。デフォルトは false
characterDataOldValue 対象ノードの変更前のテキストデータを記録する。デフォルトは false

target と options

observe() メソッドの引数には、どこ(target)で発生したどのような変化を検知するのか(options)を指定します。この指定が適切でないと、期待する変化を検知できない可能性があります。

以下は、引数の監視対象と options の subtree の指定方法により、異なる検知結果になる例です。

どのような変化を検知したかを確認するために、検知した変化の内容(MutationRecord オブジェクト)をコンソールに出力する以下のようなオブザーバーを生成します。

//コールバック関数
const callback = (mutations) => {
  //引数の mutations は DOM の変化の内容を表す MutationRecord オブジェクトの配列
  mutations.forEach((mutation) => {
    //各 MutationRecord をコンソールに出力
    console.log(mutation);
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

以下のような HTML があり、h2 要素のテキストの変更を検出する場合、

<div id="foo">
  <h2>Mutation Observer</h2>
</div>

以下を JavaScript に追加して変更を監視します。

監視対象を h2 要素にして、監視のオプションの childList: true を指定すると h2 要素の子ノード(この場合は Text ノード)の変更を検出することができます。

以下では textContent を使って h2 要素のテキストを変更しています。

//監視対象を h2 要素に
const target = document.querySelector('h2')

//監視のオプション
const config = {
  childList: true, //対象ノードの子ノードに対する追加・削除の監視を有効に
};

//監視を開始
observer.observe(target, config);

// h2 要素のテキストを変更
document.querySelector('h2').textContent = 'New Title';

上記の場合、コンソールには以下が出力され、target は h2、 addedNodes(追加されたノード)が NodeList[text]、 removedNodes(削除されたノード)が NodeList[text] になっていて、上記のテキストの変更は h2 要素のテキストノードが削除・追加される変化として検知されています。

以下のように監視の対象を id="foo" の div 要素(div#foo)に変更すると、h2 要素は div#foo の子孫ノードですが、 h2 要素のテキストは h2 要素の子ノードで、div#foo の子孫ノードではないため、テキストは変更されますが、オブザーバーは変化を検知しません(コンソールには何も出力されません)。

//監視対象を div#foo に変更
const target = document.getElementById('foo');
const config = {
  childList: true,
};
observer.observe(target, config);
// h2 要素のテキストを変更(変化は検知されない)
document.querySelector('h2').textContent = 'New Title';

h2 要素のテキストを textContent で変更するのではなく、以下のように h2 要素自体を置換すると、div#foo の子ノードの変更が発生するので、オブザーバーは変化を検知します。

//h2 要素の生成
const newElem = document.createElement('h2');
//テキストノードを生成して作成した要素に追加
newElem.appendChild(document.createTextNode('New Title'));
//置換対象の親ノードを取得
const parentNode = document.getElementById('foo');
//置換対象のノード (#foo の最初の子要素)
const oldElem = parentNode.firstElementChild;
//既存の h2 要素を生成した h2 要素に置換してテキストを変更
parentNode.replaceChild(newElem, oldElem);

コンソールには以下が出力され、target は div#foo、 addedNodes が NodeList[h2]、 removedNodes が NodeList[h2] になっていて、上記のテキストの変更(h2 要素の置換)は h2 要素が削除・追加される変化として検知されています。

または、監視のオプションの subtree: true を追加で指定して子孫ノードに対する変更の監視を有効にすると、h2 要素も監視の対象になるので、以下の場合でもオブザーバーは変更を検知します。

//監視対象は div#foo
const target = document.getElementById('foo');
const config = {
  childList: true,
  subtree: true, //対象ノードとその子孫ノードに対する変更の監視を有効に
};
observer.observe(target, config);

// h2 要素のテキストを変更
document.querySelector('h2').textContent = 'New Title';

コンソールには以下が出力され、target は h2、 addedNodesが NodeList[text]、 removedNodes が NodeList[text] になっていて、最初の例と同じ変化として検知されています。

監視対象と subtree の指定以外にも、どのような変化を検知するかのオプション(childList、attributes、characterData )の指定により、検知する結果が異なってきます。

文書中の全てのノードの変化を検知

監視対象を document にして、 subtree: true を含む全てのオプションを指定すれば、文書中の全てのノードの変化(ノードの追加・削除、属性の変更、テキストデータの変更)を検知できます。

const observer = new MutationObserver( (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);  //各 MutationRecord をコンソールに出力
  })
}).observe( document, {  // 対象を document に
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,  // 子孫ノードも監視
});

childList

childList を true に指定すると、対象ノードの子ノード(テキストノードを含む)に対する変更(追加・削除)の監視を有効にします。

先の例同様、どのような変化を検知したかを確認するために、検知した変化の内容(MutationRecord オブジェクト)をコンソールに出力する以下のようなオブザーバーを生成します。

//コールバック関数
const callback = (mutations) => {
  mutations.forEach((mutation) => {
    //各 MutationRecord をコンソールに出力
    console.log(mutation);
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

以下のような HTML がある場合、

<div id="foo">
  <h2>Mutation Observer</h2>
</div>

以下を JavaScript に追加して変更を監視します。

監視対象を id="foo" の div 要素にして、監視のオプションの childList: true を指定すると div#foo の子ノードの変更(追加・削除)を検出することができます。

//監視対象のノード
const target = document.getElementById('foo');

//監視のオプション
const config = {
  childList: true, //対象ノードの子ノードに対する追加・削除の監視を有効に
};
//監視を開始
observer.observe(target, config);

上記の後に以下のような変更を追加します。

  • p 要素を生成して、div#foo の子ノードとして追加(1〜10行目)
  • div#foo に追加した p 要素のテキスト(子ノード)を変更(13行目)
  • h2 要素のテキスト(子ノード)を変更(18行目)
  • p 要素のスタイル属性を変更(21行目)
  • h2 要素のテキストデータ(子ノードの data プロパティ)を変更(24行目)
//p 要素を生成
const p = document.createElement('p');
//生成した p 要素に id を指定
p.id = 'bar';
//テキストノードを生成(表示するテキスト)
const text = document.createTextNode('Hello from Bar!');
//要素ノード(p 要素)の直下にテキストノードを追加
p.appendChild(text);
//div#foo(監視対象のノード)に生成したノードを追加(※ この変化は検知される)
document.getElementById('foo').appendChild(p);

// div#foo に追加した p 要素のテキストを変更
p.textContent = 'Text changed!'

//h2 要素
const h2 = document.querySelector('h2');
//h2 要素のテキストを変更
h2.textContent = 'Title changed!'

//p 要素のスタイル属性を変更
p.style.color = 'red';

//h2 要素のテキストデータ(firstChild.data)を変更
h2.firstChild.data = 'Title changed again!'

この場合、observe() の引数で、監視対象を div#foo、監視オプションを childList: true としているので、div#foo に p 要素を子ノードとして追加した変更(10行目)のみが検知され、以下がコンソールに出力されます。

  • addedNodeds: NodeList[p#bar] 追加されたノード(p 要素)
  • removedNodes: NodeList[] 削除されたノード(削除されていないので空のノードリスト)
  • target: div#foo 監視対象
  • type: "childList" 発生した変更のタイプ

監視のオプションに subtree: true を追加して、対象ノードとその子孫ノードに対する変更の監視を有効にすると、

//監視のオプション
const config = {
  childList: true,
  subtree: true, //追加
};

子孫ノードも監視されるので、p 要素のテキストの変更と、h2 要素のテキストの変更も検知されます。

この場合、テキストの変更はテキストノード(対象ノードの子ノード)の削除と追加の変化として検知されています。以下は p 要素の例ですが、h2 要素の場合も同じです。

  • addedNodeds: NodeList[text] 追加されたノード(テキストノード)
  • removedNodes: NodeList[text] 削除されたノード(テキストノード)
  • target: p#bar 監視対象
  • type: "childList" 発生した変更のタイプ

監視のオプションに attributes: true と characterData: true を追加して、対象ノードの属性に対する変更と対象ノードのテキストデータの変更の監視を有効にすると、

//監視のオプション
const config = {
  childList: true,
  subtree: true,
  attributes: true, //追加
  characterData: true, //追加
};

p 要素のスタイル属性の変更と h2 要素のテキストデータの変更も検知されます。

p 要素のスタイル属性の変更

  • attributeName: style 変更された属性(style)
  • target: p#bar 監視対象
  • type: "attributes" 発生した変更のタイプ

h2 要素のテキストデータの変更

  • target: text 監視対象(テキストデータ)
  • type: "characterData" 発生した変更のタイプ

発生した変更のタイプが childList ではないので(該当する値がないため)、いずれの情報の addedNodeds と removedNodes は空のノードリストになっています。また、その他の値が null になっているプロパティは、発生した変更のタイプにおいて該当する値がないことを意味します。

変更前の値

attributeOldValue:true を指定すると変更前の属性値を記録し、characterDataOldValue:true を指定すると変更前のテキストデータを記録します。

//監視のオプション
  const config = {
    childList: true,
    subtree: true,
    attributeOldValue: true, //変更前の属性値を記録
    characterDataOldValue: true, //変更前のテキストデータを記録
  };

上記のようにオプションを変更すると、出力は以下のようになります。

この例の場合、type:attributes のp#bar の変更前に style 属性は設定されていないので、null になっています(変更前に style が設定されていれば oldValue にセットされます)。

type:characterData の検知の oldValue は確認できます。

childList の変更前の値

type:childList の oldValue は常に null ですが、変更前の値は removedNodes を展開すると確認することができます。この例の場合はノードリストの最初のノードなので、0 を展開すると、data と nodeValue、textContent プロパティに値がセットされているのが確認できます。

以下は type:childList の変更前の値をコールバック関数で出力する例です。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
    // removedNodes に最初の要素がセットされていれば、
    if(mutation.removedNodes[0]) {
      //data を出力(または nodeValue や textContent でも同じ)
      console.log(mutation.removedNodes[0].data)
    }
  })
}

関連項目:変更前のテキストの値

characterData

characterData を true に指定すると、対象ノードのテキストノードのテキスト(data または nodeValue プロパティ)の変更の監視を有効にします。

先の例同様、どのような変化を検知したかを確認するために、検知した変化の内容(MutationRecord オブジェクト)をコンソールに出力する以下のようなオブザーバーを生成します。

//コールバック関数
const callback = (mutations) => {
  mutations.forEach((mutation) => {
    //各 MutationRecord をコンソールに出力
    console.log(mutation);
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

以下のような HTML がある場合、

<div id="foo">
  <h2>Mutation Observer</h2>
</div>

以下のように監視の対象を h2 要素とし、監視のオプションに対象ノードのテキストデータの変更を監視する characterData: true を指定して、h2 要素のテキストを textContent で変更してもオブザーバーは変化を検知しません。

characterData: true は、対象ノードのテキストデータ(characterData を継承する Text や Comment など)の変更を監視します。

対象ノードのテキストデータとは、対象ノードの子ノードであるテキストノードのテキスト(data または nodeValue プロパティ)になります。

以下の場合は対象ノードの子ノードの変更(テキストノードの削除・追加によるテキストの変更)になるため、変化を検知しません。

また、テキストノードのテキストは子孫ではない(テキストノードは子を持つことができない)ので、この場合は subtree: true を指定しても意味がありません。

//監視対象のノード
const target = document.querySelector('h2');
//監視のオプション
const config = {
  characterData: true, //対象ノードのテキストデータの変更の監視を有効に
  subtree: true, //対象ノードとその子孫ノードに対する変更の監視を有効に
};
observer.observe(target, config);

// h2 要素のテキストを変更
document.querySelector('h2').textContent = 'New Title';

以下のように、h2 要素のテキストをテキストノード(firstChild)の data やnodeValue、textContent プロパティ を使って変更した場合は、オブザーバーは変化を検知します。

この場合は、監視対象を h2 要素としているので、subtree: true の指定も必要になります。

//監視対象のノード
const target = document.querySelector('h2');
//監視のオプション
const config = {
  characterData: true,
  subtree: true, //この場合は、この指定が必要
};
observer.observe(target, config);

//テキストノードの data または nodeValue を変更
document.querySelector('h2').firstChild.data = 'New Title';
//または以下でも同じ
//document.querySelector('h2').firstChild.nodeValue = 'New Title';
//document.querySelector('h2').firstChild.textContent = 'New Title';

上記の場合、コンソールには以下が出力され、target は text(テキストノード)に、type は characterData になっています。 type が characterDataの場合、addedNodes と removedNodes は空のノードリスト(NodeList[])になります。

subtree: true を指定しない場合は、監視対象をテキストノードにする必要があります。

//監視対象をテキストノードに
const target = document.querySelector('h2').firstChild;
const config = {
  characterData: true,
};
observer.observe(target, config);

// テキストノードの data または nodeValue を変更
document.querySelector('h2').firstChild.data = 'New Title';
characterDataOldValue

characterData: true を指定する代わりに、characterDataOldValue: true を指定すれば、変更前のテキストデータを記録することができます。

コールバック内では、変更前のテキストデータは .oldValue でアクセスできます。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
    //変更前のテキストデータを出力
    console.log('mutation.oldValue: ' + mutation.oldValue)
  })
}
const observer = new MutationObserver(callback);
const target = document.querySelector('h2').firstChild;
const config = {
  characterDataOldValue: true, //変更前のテキストデータを記録
};
observer.observe(target, config);
document.querySelector('h2').firstChild.data = 'New Title';

Text ノード

以下の場合、h2 要素のテキストノードは以下のいずれかで確認できます。

<h2>Mutation Observer</h2>
<script>
console.dir(document.querySelector('h2').childNodes[0])
console.dir(document.querySelector('h2').firstChild)
console.dir(document.querySelector('h2').lastChild)
</script>

#text を展開すると、data やnodeValue、textContent プロパティが確認できます。また、nodeName は #text、nodeType は 3 であることも確認できます。

contentEditable

contenteditable 属性を指定した編集可能な要素を characterData:true や characterDataOldValue:true を指定して、入力されたテキストによる変更を検知することもできます。

<div class="editable" contentEditable>Edit Here</div>

以下を記述して、ブラウザからその領域のテキストを編集すると、変更が検知されてコンソールに変更の情報が出力されます。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation);
  })
}
const observer = new MutationObserver(callback);
//監視対象を contenteditable を指定した要素のテキストノードに
const target = document.querySelector('.editable').firstChild
//監視のオプション
const config = {
  characterDataOldValue: true,
};
observer.observe(target, config);

attributes

attributes を true に指定すると、対象ノードの属性に対する変更の監視を有効にします。

先の例同様、どのような変化を検知したかを確認するために、検知した変化の内容(MutationRecord オブジェクト)をコンソールに出力する以下のようなオブザーバーを生成します。

//コールバック関数
const callback = (mutations) => {
  mutations.forEach((mutation) => {
    //各 MutationRecord をコンソールに出力
    console.log(mutation);
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

以下のような HTML があり、 h2 要素の属性の変更を検知する場合、

<div id="foo">
  <h2 class="primary">Mutation Observer</h2>
</div>

監視対象を h2 要素にして、監視のオプションの attributes: true を指定すると h2 要素の属性の変更を検出することができます。

以下では h2 要素に classList.add() でクラス属性を追加しています。

//監視対象を h2 要素に指定
const target = document.querySelector('h2.primary');
//監視のオプション
const config = {
  attributes: true,  //対象ノードの属性に対する変更の監視を有効に
};
//監視を開始
observer.observe(target, config);

//h2 要素にクラスを追加
document.querySelector('h2.primary').classList.add('mt-3')

コンソールには以下が出力され、type は attributes、attributeName(属性名)は class、target は h2.primary.mt-3 になっています。

addedNodes と removedNodes は NodeList[](空のノードリスト)になっていています。

attributeFilter

attributes: true の代わりに attributeFilter: 属性名の配列 を使うと、配列で指定した属性名の属性のみを検知します。

以下の場合、class、title、style 属性を追加していますが、attributeFilter:['class', 'style'] としているので、class と style 属性の変更のみが検知されます。

const config = {
  attributeFilter:['class', 'style']  //class と style 属性のみを監視
};
observer.observe(target, config);

const h2 = document.querySelector('h2.primary');
h2.classList.add('mt-3');  // class 属性を追加
h2.setAttribute('title', 'Mutation Observer');  // title 属性を追加
h2.style.color = 'red';  // style 属性を追加

attributeOldValue

attributes: true の代わりに attributeOldValue: true を使うと、変更前の属性値を記録することができます。

コールバック関数内では、変更前の属性値は .oldValue でアクセスできます(characterDataOldValue と同じプロパティ)。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    //変更前の属性値を出力
    console.log('mutation.oldValue: ' + mutation.oldValue)
    console.log(mutation);
  })
}
const observer = new MutationObserver(callback);
const target = document.querySelector('h2.primary');

const config = {
  attributeOldValue: true,   //変更前の属性値を記録
};
observer.observe(target, config);
document.querySelector('h2.primary').classList.add('mt-3')

input 要素の属性

監視オプションの attributes や attributeFilter、attributeOldValue を有効にして、input 要素の value 属性や checked 属性を JavaScript から操作した変更を検知することができます。

ブラウザから入力された変更やチェックされた変更などは検知できませんが、それらは通常の input 要素の change や input などのイベントが使えます。

例えば、以下のような HTML がある場合、

<div id="foo">
  <input type="text" id="name" value="name">
  <input type="checkbox" id="agree" value="agreed"> 同意する
</div>

以下のように監視のオプションに attributes: true を指定すると、input 要素の JavaScript からの変更を検知することができます。

この例では2つの input 要素を監視するので、監視対象を親要素の div#foo にして、subtree: true を指定しています。

const callback = (mutations) => {
  mutations.forEach((mutation) => {
    //各 MutationRecord をコンソールに出力
    console.log(mutation);
  })
}
const observer = new MutationObserver(callback);

//親要素の div#foo を監視対象に
const target = document.getElementById('foo');

//監視のオプション
const config = {
  attributes: true,  //属性に対する変更の監視を有効にし、変更前の値を記録
  subtree: true,  //子孫ノードに対する変更の監視を有効に
};
observer.observe(target, config);

// テキストフィールドの value 属性を変更
document.getElementById('name').setAttribute('value', 'Foo');
// チェックボックスの checked 属性を変更
document.getElementById('agree').setAttribute('checked', true)
// 但し、以下では検知されない
//document.getElementById('agree').checked = true; //検知されない

テキストフィールドの value 属性とチェックボックスの checked 属性の変更が検知されて、以下のように出力されます。

コールバック関数

コンストラクター MutationObserver() に渡すコールバック関数は以下の2つの引数を受け取ります。

引数 説明
mutations 発生した変化の情報を記述した MutationRecord オブジェクトの配列
observer コールバックを実行した MutationObserver のインスタンス(MutationObserver 自身への参照)。必要に応じてコールバック内から自身へアクセスできます(特定の条件で監視を停止・再開する場合など)。必要ない場合は省略することができます。
構文
const callback = (mutations, observer) => {
  //DOM に変更があった場合に実行する処理
}

MutationRecord

コールバック関数の第一引数 mutations には、発生した DOM の変化の情報を格納した MutationRecord オブジェクトの配列が渡されます。

MutationRecord オブジェクトは以下のようなプロパティがセットされます。

発生した変化(ミューテーション)の type により各プロパティにセットされる値は異なります。

例えば、type が childList の場合は addedNodes と removedNodes のいずれか、または両方のノードリストにノードが含まれますが、その他の type では空のノードリスト(NodeList[])にります。

MutationRecord の構造は type に関わらず一定なので、type と target 以外の大部分のプロパティの値には null や空のノードリスト(NodeList[])がセットされています。

MutationRecord のプロパティ
プロパティ 型 説明
type String 発生した DOM の変更の種類を表す以下のいずれかの文字列。
  • attributes: 属性値に対する変更
  • characterData:テキストデータ(data)に対する変更
  • childList:ノードツリーに対する変更
target Node 変更の影響を受けたノード(type に応じて以下のいずれか)。
  • attributes の場合:属性が変更された要素
  • characterData の場合:CharacterData ノード(text など)
  • childList の場合:子ノードが変更されたノード(親ノード)
addedNodes NodeList 追加されたノードのリスト。※ 複数のノードが入っている可能性があります(例えば、DocumentFragment を追加した場合など)。何もノードが追加されていない場合は空のノードリスト NodeList[]
removedNodes NodeList 削除されたノードのリスト。※ 複数のノードが入っている可能性があります。何もノードが削除されていない場合は空のノードリスト NodeList[]
previousSibling Node 追加あるいは削除されたノードの直前にあるノード。該当するノードがなければ null
nextSibling Node 追加あるいは削除されたノードの直後にあるノード。該当するノードがなければ null
attributeName String 変更された属性の名前。該当する属性がなければ null
attributeNamespace String 変更された属性の名前空間(XML の場合)。該当がなければ null
oldValue String 変更前の値(type に応じて以下のいずれか)。
  • attributes の場合:変更された属性の変更前の属性値
  • characterData の場合:変更されたノードの変更前のデータ
  • childList の場合:null
※ この機能が動作するためには observe() メソッドの第二引数で、attributeOldValue または characterDataOldValue が true に設定されている必要があります。

コールバック関数では、第一引数に渡される MutationRecord の配列を調べることで、発生した変更の情報にアクセスすることができます。

以下は body 及びその子孫ノードの変更を監視して、変更を検知したらタイプごとに変更の内容をコンソールに出力する例です。

type が childList の場合は、addedNodes 及び removedNodes には複数のノードが含まれている可能性があるので、forEach() で各ノードを調べています。type が childList 以外の場合は、addedNodes 及び removedNodes は空のノードリストがセットされます。

document.addEventListener('DOMContentLoaded', ()=> {
  //body 及びその子孫ノードを監視して変更を検知したらタイプごとにコンソールに出力
  new MutationObserver((mutations) => {
    //MutationRecord の配列の個々の要素を調べる
    for(const mutation of mutations) {
      console.log('*** 変更の種類: ' + mutation.type + " ***");
      console.log('変更を検知したノード:' + mutation.target.nodeName);
      // type が childList の場合
      if ( mutation.type == 'childList' ) {
        // addedNodes のノードリストにノードが含まれれば
        if (mutation.addedNodes.length >= 1) {
          mutation.addedNodes.forEach((node) => {
            console.log('追加されたノード: ' + node.nodeName);
            //ノードが要素の場合
            if(node.nodeType ===1 && node.textContent) {
              console.log('テキスト: ' + node.textContent);
            }
            //ノードがテキストノードの場合
            else if(node.nodeType ===3) {
              console.log('テキスト: ' + node.data);
              console.log('親ノード: ' + node.parentNode.nodeName);
            }
          })
        }
        // removedNodes のノードリストにノードが含まれれば
        if (mutation.removedNodes.length >= 1) {
          mutation.removedNodes.forEach((node) => {
            console.log('削除されたノード: ' + node.nodeName);
            //ノードが要素の場合
            if(node.nodeType ===1 && node.textContent) {
              console.log('テキスト: ' + node.textContent);
            }
            //ノードがテキストノードの場合
            else if(node.nodeType ===3) {
              console.log('テキスト: ' + node.data);
            }
          })
        }
      }
      // type が characterData の場合
      else if (mutation.type == 'characterData') {
        console.log('変更されたノード: ' + mutation.target.nodeName);
        console.log('変更されたテキストデータ: ' + mutation.target.data );
      }
      // type が attributes の場合
      else if (mutation.type == 'attributes') {
        console.log('変更されたノード: ' + mutation.target.nodeName);
        console.log('変更された属性: ' + mutation.attributeName);
      }
    }
  }).observe(document.body, { //監視対象 を document.body に
    // すべてのタイプの監視を有効にし、子孫ノードの監視も有効に
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true,
  });
});

コールバック関数の例

以下は、div#container の直下にテキストノードが追加されたら、p 要素でラップする例です。

HTML
<body>
  <div id="container"></div>
</body>

div#container 直下へのテキストノードの追加を監視するので、target には div#container を指定し、オプションは childList: true を指定します。

コールバック関数では、MutationRecord の type が childList の場合は、その addedNodes を調べます。addedNodes は複数のノードが入っている可能性があるノードリストなので forEach() で各ノードを調べ、テキストノードであれば処理を実行します。

JavaScript
//コールバック関数
const callback = (mutations) => {
  //引数の mutations は MutationRecord の配列なので forEach でループ
  mutations.forEach((mutation) => {
    //console.log('called');
    // MutationRecord の type が childList であれば
    if ( mutation.type === 'childList' ) {
      // addedNodes(ノードリスト)にノードが入っていれば
      if (mutation.addedNodes.length >= 1) {
        // addedNodes に含まれている各ノードを調べる
        mutation.addedNodes.forEach((node) => {
          //console.log(node.nodeType);
          // nodeType が 3(テキストノード)であれば
          if(node.nodeType === 3) {
            //p 要素を生成
            const p = document.createElement('p');
            //p.appendChild(node); //これだとエラーになる
            //生成した p 要素に追加されたテキストノードのテキストを設定
            p.innerText = node.data
            //追加されたテキストノードを p 要素に置換
            node.parentNode.replaceChild(p, node);
          }
        })
      }
    }
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

//監視対象を div#container に
const target = document.getElementById('container')
//対象ノードの子ノードに対する追加・削除の監視を有効に
const config = {
  childList: true,
};
observer.observe(target, config);

例えば、以下のような変更を記述すると、div#container の直下へのテキストノードの追加なので、変更は検知され、追加されたテキストは p 要素でラップされます。

テキスト以外のノードを追加した場合も childList: true によりコールバック関数は呼び出されますが、 p 要素でラップする処理は実行されません。

//テキストノードを生成(表示するテキスト)
const text = document.createTextNode('Hello document!');
//監視対象( div#container)の直下にテキストノードを追加
document.getElementById('container').appendChild(text);

ノードの種類

ノードの種類は nodeType で判定できます。以下は一般的なノードの種類です。

※ 要素はノードの種類の1つです

一般的なノードの種類
ノード インターフェース nodeType 定数 nodeType の値
要素ノード Element Node.ELEMENT_NODE 1
属性ノード Attr Node.ATTRIBUTE_NODE 2
テキストノード Text Node.TEXT_NODE 3
コメントノード Coment Node.COMMENT_NODE 8
文書ノード Document Node.DOCUMENT_NODE 9

変更前のテキストの値

以下は div#container の配下の要素でテキストが変更された場合に、変更前のテキストの値をコンソールに出力する例です。

テキストの変更を検知するには、監視オプションに characterDataOldValue:true と childList:true を指定し、対象のノードの子孫ノードも監視対象にするので subtree:true も指定します。

対象ノードのテキストデータが変更された場合は、characterDataOldValue:true により oldValue プロパティで変更前のテキストの値を参照できますが、要素の textContent でテキストを変更した場合は、oldValue プロパティの値は null なので、removedNodes を調べます(変更前の値)。

HTML
<div id="container">
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. </p>
  <p>Tenetur at magnam dolores corporis architecto esse.</p>
</div>

MutationRecord の type が childList の場合は removedNodes から、characterData の場合は oldValue から変更前の値を取得できます。

JavaScript
//コールバック関数
const callback = (mutations) => {
  //引数の mutations は MutationRecord の配列なので forEach でループ
  mutations.forEach((mutation) => {
    // MutationRecord の type が childList であれば
    if ( mutation.type === 'childList' ) {
      // removedNodes にノードが入っていれば
      if (mutation.removedNodes.length >= 1) {
        // removedNodes に含まれている各ノードを調べる
        mutation.removedNodes.forEach((node) => {
          // data プロパティ(テキスト)を出力
          console.log('Old Value : ' + node.data);
        })
      }
    }
    // MutationRecord の type が characterData であれば
    else if ( mutation.type === 'characterData' ) {
      // oldValue プロパティを出力
      if(mutation.oldValue) console.log('Old Value : ' + mutation.oldValue)
    }
  })
}
//MutationObserver のインスタンス(オブザーバー)を生成
const observer = new MutationObserver(callback);

//監視対象を div#container に
const target = document.getElementById('container')
//監視のオプション
const config = {
  childList: true,
  characterDataOldValue: true,
  subtree: true
};
observer.observe(target, config);

//textContent でテキストを変更
document.querySelectorAll('#container p')[0].textContent = '1st paragraph.'
//テキストノードの data プロパティでテキストを変更
document.querySelectorAll('#container p')[1].firstChild.data = '2nd paragraph';

MutationObserver の使用例

例えば、ページ(ドキュメント)を読み込んだ時点で要素に対してスクリプトを適用している場合、後から動的に追加した要素にはスクリプトは適用されていないので、その効果は反映されません。

そのような場合、MutationObserver を使うと、ページに動的に挿入された要素を検出して、自動的にスクリプトを再度実行することで追加された要素を含むページ全体に効果を適用させることができます。

以下は、対象となる要素が動的に追加された際に MutationObserver を使ってページ読み込み時に実行している関数を再度実行して適用させる例です。

この例の場合、ページ読み込み時に独自に定義したスクロールスパイの関数とスムーススクロールの関数を実行していますが、そのままでは動的に追加した要素は効果が反映されません。

以下では、body 及びその子孫ノードに linkTarget クラスを指定した div 要素を追加する変更を検知すると、自動的にスクロールスパイの関数 ioScrollSpy() とリンク項目を追加してスムーススクロールを適用する関数 addNavigationItem() を実行してページに適用します。

挿入された要素が div.linkTarget かをチェックするには matches() メソッドを使用しています。

18〜23行目は、追加されたノードの子ノードとして div.linkTarget が含まれている場合にも対応するための記述です。

//MutationObserver のコールバック関数
const callback = (mutationsList) => {
  mutationsList.forEach((mutation) => {
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        for(let node of mutation.addedNodes) {
          // 要素ノードのみを対象とし、他のノード(例 テキストノード)はスキップ
          if (!(node instanceof HTMLElement)) continue;
          //if (!(node.nodeType === 1)) continue;
          // 挿入された要素が linkTarget クラスの div 要素かをチェック
          if (node.matches('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
          // サブツリーのどこかに linkTarget クラスの div 要素がある場合
          for(let elem of node.querySelectorAll('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
        }
      }
    }
  });
}
// コールバック関数を渡してオブザーバーを生成
mo = new MutationObserver(callback);
// 監視対象とオプションを指定して監視を開始
mo.observe( document.body, {
  childList: true, //子ノードに対する変更(追加・削除)の監視を有効に
  subtree: true, //子孫ノードに対する変更の監視を有効に
});

上記のコールバック関数部分は forEach() を使って以下のように記述しても同じです。

const callback = (mutationsList) => {
  mutationsList.forEach((mutation) => {
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        mutation.addedNodes.forEach((node) => {
          // node が要素要素であれば(node.nodeType === 1 と同じ)
          if(node instanceof HTMLElement) {
            // 挿入された要素が linkTarget クラスの div 要素であれば
            if (node.matches('div.linkTarget')) {
              //スクロールスパイの関数を実行
              ioScrollSpy();
              //ナビゲーションにリンク項目を追加する関数を実行
              addNavigationItem();
            }
            // 挿入された要素のサブツリーに linkTarget クラスの div 要素があれば取得
            const linkTarget = node.querySelectorAll('div.linkTarget');
            if(linkTarget.length >=1 ) {
              linkTarget.forEach( () => {
                ioScrollSpy();
                addNavigationItem();
              })
            }
          }
        })
      }
    }
  });
}

サンプルを開く

上記サンプルでは、ボタンをクリックすると div.linkTarget を追加します(実際にはこのような使い方はしないと思います)。

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>IntersectionObserver with MutationObserver 1</title>
<style>
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
html {
  color: #444;
}
body {
  margin-top: 50px;
}
h1 {
  color: #999;
  font-size: 24px;
   margin-bottom: 2rem;
}
h2 {
  margin-bottom: 1rem;
}
p {
  margin-bottom: .5rem;
}
.wrapper {
  max-width: 780px;
  margin: 20px auto 0;
  padding: 0 1rem;
}
.nav-wrapper {
  position: sticky;
  top: 0px;
  width: 100%;
  background-color: #fefefe;
  padding: .5rem 0;
}
.nav {
  width: 100%;
  max-width: 600px;
  display: flex;
  justify-content: flex-start;
}
.contents {
  width: 100%;
}
.nav {
  padding-left: 0;
}
.nav li {
  list-style: none;
  color: #70B466;
}
.nav a {
  display: block;
  width: 100%;
  padding: .25rem .5rem;
  text-decoration: none;
  color: #70B466;
}
.nav a.active {
  color: #D0F0C1;
  background-color: #557E49;
}
.linkTarget {
  margin-top: 4rem;
}
.footer {
  width: 100vw;
  height: 400px;
  background-color: #eee;
  margin-top: 300px;
}
/*トップへスクロールするボタン*/
#scroll-to-top {
  position: fixed;
  right: 15px;
  bottom: 2rem;
  z-index: 100;
  font-size: 0.75rem;
  background-color: #557E49;
  width: 80px;
  height: 50px;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
  color: #fff;
  line-height: 50px;
  text-align: center;
  transition: opacity .4s;
  opacity: .7;
  cursor: pointer;
}
#scroll-to-top:hover {
  opacity: 1;
}
#scroll-to-top p {
  margin-top: 10px;
}
.controls button {
  margin: 20px 20px 0 20px;
}
#addContent {
  background-color: rgb(194, 240, 244);
  border: 1px solid #8394af;
  padding: 8px;
}
.controls input:checked + label {
  color: red;
}
</style>
</head>

<body>
<div class="wrapper">
  <h1>IntersectionObserver with MutationObserver 1</h1>
  <nav class="nav-wrapper">
    <ol id="navigation" class="nav">
      <li><a href="#section1">Section 1</a></li>
      <li><a href="#section2">Section 2 </a></li>
      <li><a href="#section3">Section 3</a></li>
    </ol>
  </nav>
  <div class="controls">
    <button id="addContent">Add Content</button>
  </div>
  <main class="contents">
    <div class="linkTarget" id="section1">
      <h2>Section 1</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempora dolores quam a dolorem, architecto molestias fugit necessitatibus, deserunt doloremque! Laborum voluptatum, maiores temporibus ab pariatur reiciendis ipsa ad tenetur in.</p>
      <p>Possimus, ipsam a vero quae tempora molestias autem quas quisquam officiis distinctio recusandae, et, consectetur blanditiis maxime, eaque inventore ut. Aut facere, quae architecto unde, dolores autem est ratione voluptatum.</p>
    </div>
    <div class="linkTarget" id="section2">
      <h2>Section 2</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorem natus quos voluptatibus tempore repudiandae aut, vero dolorum magni, exercitationem magnam eveniet, omnis soluta doloremque atque iusto provident expedita sint enim.</p>
      <p>Commodi nulla cupiditate rerum culpa at aspernatur dolorem iusto fuga officiis magni, nihil accusamus impedit repellendus obcaecati quod optio, odit reiciendis porro minima explicabo nesciunt earum, facilis quos ut accusantium!</p>
    </div>
    <div class="linkTarget" id="section3">
      <h2>Section 3</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis provident facere molestiae exercitationem eligendi quam perferendis quae enim sed omnis harum rem in officiis veritatis aliquid, corrupti veniam velit deleniti.</p>
      <p>Delectus iusto repellendus quas ipsam! Veritatis a, facilis architecto necessitatibus consequuntur omnis neque aliquid quam exercitationem laboriosam quos magni error numquam enim temporibus, iure cumque modi ipsam soluta dignissimos. Nam!</p>
      <p>Nobis consequatur, esse repudiandae soluta excepturi maiores a in quisquam est natus molestias aspernatur dignissimos, asperiores quasi? Quae nulla mollitia, vero nam eaque incidunt cupiditate consectetur. Repellendus, est dignissimos qui.</p>
    </div>
  </main>
</div>
<div class="footer"></div>
<div id="scroll-to-top"><!--トップへスクロールするボタン-->
  <p>Top</p>
</div>
<script>
//ナビゲーションにリンク項目を追加する関数
const addNavigationItem = () => {
  // .linkTarget の要素の数
  const sectionsCount = document.querySelectorAll('.linkTarget').length;
  // 最後の .linkTarget の要素(追加された要素)
  const addedSection = document.querySelectorAll('.linkTarget')[sectionsCount-1];
  // 最後の .linkTarget の要素(追加された要素)のタイトルのテキスト
  const addedSectionTitle = addedSection.querySelector('h2').textContent;
  //li 要素(リンク項目)の生成
  const li = document.createElement('li');
  //innerHTML で子要素(リンク)を追加
  li.innerHTML = `<a href="#section${sectionsCount}">${addedSectionTitle}</a>`;
  //追加先のノードに生成した要素(リンク項目)を追加
  document.getElementById('navigation').appendChild(li);
  //スムーススクロールの関数を実行
  applySmoothScroll(document.querySelectorAll('a[href^="#"]'));
}

/****** MutationObserver の設定 ******/

//MutationObserver のコールバック関数
const callback = (mutationsList) => {
  mutationsList.forEach((mutation) => {
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        for(let node of mutation.addedNodes) {
          // 要素ノードのみを対象とし、他のノード(例 テキストノード)はスキップ
          if (!(node instanceof HTMLElement)) continue;
          //if (!(node.nodeType === 1)) continue;
          // 挿入された要素が linkTarget クラスの div 要素かをチェック
          if (node.matches('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
          // サブツリーのどこかに linkTarget クラスの div 要素がある場合
          for(let elem of node.querySelectorAll('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
        }
      }
    }
  });
}
// コールバック関数を渡してオブザーバーを生成
mo = new MutationObserver(callback);
// 監視対象とオプションを指定して監視を開始
mo.observe( document.body, {
  childList: true, //子ノードに対する変更(追加・削除)の監視を有効に
  subtree: true, //子孫ノードに対する変更の監視を有効に
});

//クリックで linkTarget クラスの div 要素を追加するリスナー
document.getElementById('addContent').addEventListener('click', () => {
  //Section の番号(現在の.linkTargetに div を追加した後の数なので1増加)
  let sectionsCount = document.querySelectorAll('.linkTarget').length + 1;
  //div 要素の生成
  const div = document.createElement('div');
  //生成した div 要素にクラス属性を設定
  div.className = 'linkTarget';
  div.id = `section${sectionsCount}`;
  //innerHTML で子要素を追加
  div.innerHTML = `<h2>Section ${sectionsCount}</h2>
  <p>Maiores labore quidem est nemo quia ullam deleniti, ipsum voluptatibus dolorem. Explicabo nisi, possimus. Iusto alias, totam sunt excepturi tempora qui modi autem, ipsum aut doloribus facilis, possimus deserunt atque.</p>
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempora dolores quam a dolorem, architecto molestias fugit necessitatibus, deserunt doloremque! Laborum voluptatum, maiores temporibus ab pariatur reiciendis ipsa ad tenetur in.</p>`;
  //追加先のノードに生成した要素を追加
  document.querySelector('main.contents').appendChild(div);
});

/****** スクロールスパイの関数 ******/
const ioScrollSpy = (rootSelector=null, rootMargin='-25% 0px -75%', targetClass='linkTarget', navSelector='#navigation', activeClass='active') => {
  //監視対象の要素を取得
  const targets = document.querySelectorAll('.' + targetClass);
  //オプション
  const options = {
    root: rootSelector ===null ? null : document.querySelector(rootSelector),
    //上から25%の位置を交差の境界とする
    rootMargin: rootMargin,
    threshold: 0 //デフォルトなので省略可能
  }
  //IntersectionObserver のコールバック関数
  const callback = (entries) => {
    // 交差を検知をしたら
    entries.forEach(entry => {
      //監視対象の要素が領域(境界)と交差していれば(isIntersecting が true の場合)
      if (entry.isIntersecting) {
        //その要素を引数として以下の関数を呼び出してメニューをハイライト
        highlightNavMenu(entry.target);
      }
    });
  }
  //IntersectionObserver のインスタンス(オブザーバー)を生成
  const io = new IntersectionObserver(callback, options);
  //それぞれの監視対象の要素を observe() に指定して監視
  targets.forEach( (target) => {
    io.observe(target);
  });
  //メニュー項目をハイライトする(色を変える)関数
  const highlightNavMenu = (target) => {
    //現在アクティブになっている(active クラスが付与されている)メニューの要素を取得
    const highlightedMenu = document.querySelector(navSelector + ' ' + '.' + activeClass);
    //上記で取得した要素が存在すれば、その要素から active クラスを削除
    if (highlightedMenu !== null) {
      highlightedMenu.classList.remove(activeClass);
    }
    //引数で渡された現在交差している(isIntersecting が true の)要素の id 属性からリンク先(href)の値を生成
    const href = '#' + target.id;
    //上記で生成したリンク先を持つメニューが、現在交差している要素のリンク
    const currentActiveMenu =  document.querySelector(`a[href='${href}']`);
    //現在交差している要素のリンクに active クラスを追加
    currentActiveMenu.classList.add(activeClass);
  }
}
//上記で定義した関数を呼び出す
ioScrollSpy();

/****** スムーススクロールの設定 ******/
//アニメーション(スムーススクロール)の持続時間
const duration = 800;
//開始時刻を代入する変数(最初は未定義)
let start;
//スクロール先の Y 座標(ウィンドウの左上からの座標)
let targetY;
//現在の垂直(Y)方向のスクロール量(位置)
let currentY;
//イージング関数の定義
const easeInQuad = (x) => {
  return x * x;
}
//スクロール先を調整する値(上部の固定メニューの高さ)
let offset = 80;
//コールバック関数
const smoothScroll = (timestamp) => {
  if (start === undefined) {
    start = timestamp;
  }
  //経過時間
  const elapsed = start ? timestamp - start : 0;
  //進捗度を算出してイージングを適用
  const relativeProgress =  easeInQuad( Math.min(1, elapsed / duration) );
  //移動する量(targetY)に進捗度を適用して scrollTo のY座標へ指定する値を算出
  const scrollY = currentY + targetY * relativeProgress;
  //上記で算出した位置へスクロール(上部の固定メニューの高さ分を offset で調整)
  window.scrollTo(0, scrollY - offset);
  //進捗度が1未満の場合は自身を繰り返す
  if (relativeProgress < 1) {
    requestAnimationFrame(smoothScroll);
  }
}

//href 属性が # から始まる要素(内部リンク)を全て取得
let links = document.querySelectorAll('a[href^="#"]');
//内部リンクにスムーススクロールを適用される(イベントリスナーを設定する)関数
const applySmoothScroll = (links) => {
  //内部リンクが存在すれば
  if(links.length > 0) {
    //内部リンクのそれぞれの要素に対して以下を実行
    links.forEach((elem) => {
      //それぞれの要素にクリックイベントを設定
      elem.addEventListener('click', (e) => {
        //href 属性の値を取得
        const href = e.currentTarget.getAttribute('href');
        //href 属性の値からスクロール先の要素を取得
        const target = href === "#" ?
          //href 属性の値が # の場合は対象を html 要素
          document.querySelector('html') :
          //それ以外は # 以降の部分を ID として対象の要素
          document.getElementById(href.replace('#', ''));

        //取得した要素が実際に存在すれば以下を実行
        if(target) {
          //開始時刻を初期化
          start = undefined;
          //対象(スクロール先)の要素の Y 座標(ウィンドウ座標)
          targetY =  target.getBoundingClientRect().y;
          //現在の垂直方向にスクロールされている量
          currentY = window.scrollY;
          //関数を実行
          smoothScroll();
        }
      });
    });
  }
}
//上記で定義した関数を呼び出す
applySmoothScroll (links);

//ページトップへスクロールするボタンのクリックイベント
document.getElementById('scroll-to-top').addEventListener('click', (e) => {
  //開始時刻を初期化
  start = undefined;
  //スクロール先の要素を html として Y 座標(ウィンドウ座標)を取得
  targetY =  document.querySelector('html').getBoundingClientRect().y;
  //垂直方向にスクロールされている量(位置)
  currentY  = window.scrollY;
  //関数を実行
  smoothScroll();
});
</script>
</body>
</html>

以下は上記のサンプルとほぼ同じですが、チェックボックスで監視の有効・無効を切り替えられるようにした動作確認用のサンプルです。有効・無効の切り替えは disconnect() と observe() を使っています。

チェックを外して監視を無効にした状態でコンテンツを追加した場合はスクロールスパイは適用されませんが、再度チェックを入れて有効にすると、その後追加されたコンテンツ及びその前に追加されたコンテンツにスクロールスパイが適用されます。

サンプルを開く

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>IntersectionObserver with MutationObserver 2</title>
<style>
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
html {
  color: #444;
}
body {
  margin-top: 50px;
}
h1 {
  color: #999;
  font-size: 24px;
   margin-bottom: 2rem;
}
h2 {
  margin-bottom: 1rem;
}
p {
  margin-bottom: .5rem;
}
.wrapper {
  max-width: 780px;
  margin: 20px auto 0;
  padding: 0 1rem;
}
.nav-wrapper {
  position: sticky;
  top: 0px;
  width: 100%;
  background-color: #fefefe;
  padding: .5rem 0;
}
.nav {
  width: 100%;
  max-width: 600px;
  display: flex;
  justify-content: flex-start;
}
.contents {
  width: 100%;
}
.nav {
  padding-left: 0;
}
.nav li {
  list-style: none;
  color: #70B466;
}
.nav a {
  display: block;
  width: 100%;
  padding: .25rem .5rem;
  text-decoration: none;
  color: #70B466;
}
.nav a.active {
  color: #D0F0C1;
  background-color: #557E49;
}
.linkTarget {
  margin-top: 4rem;
}
.footer {
  width: 100vw;
  height: 400px;
  background-color: #eee;
  margin-top: 300px;
}
/*トップへスクロールするボタン*/
#scroll-to-top {
  position: fixed;
  right: 15px;
  bottom: 2rem;
  z-index: 100;
  font-size: 0.75rem;
  background-color: #557E49;
  width: 80px;
  height: 50px;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
  color: #fff;
  line-height: 50px;
  text-align: center;
  transition: opacity .4s;
  opacity: .7;
  cursor: pointer;
}
#scroll-to-top:hover {
  opacity: 1;
}
#scroll-to-top p {
  margin-top: 10px;
}
.controls button {
  margin: 20px 20px 0 20px;
}
#addContent {
  background-color: rgb(194, 240, 244);
  border: 1px solid #8394af;
  padding: 8px;
}
.controls input:checked + label {
  color: red;
}
</style>
</head>
<body>
<div class="wrapper">
  <h1>IntersectionObserver with MutationObserver 2</h1>
  <nav class="nav-wrapper">
    <ol id="navigation" class="nav">
      <li><a href="#section1">Section 1</a></li>
      <li><a href="#section2">Section 2 </a></li>
      <li><a href="#section3">Section 3</a></li>
    </ol>
  </nav>
  <div class="controls">
    <input type="checkbox" name="mo" id="enableMO" value="Observe Mutation" checked>
    <label for="enableMO"> Observe Mutation </label>
    <button id="addContent">Add Content</button>
  </div>
  <main class="contents">
    <div class="linkTarget" id="section1">
      <h2>Section 1</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempora dolores quam a dolorem, architecto molestias fugit necessitatibus, deserunt doloremque! Laborum voluptatum, maiores temporibus ab pariatur reiciendis ipsa ad tenetur in.</p>
      <p>Possimus, ipsam a vero quae tempora molestias autem quas quisquam officiis distinctio recusandae, et, consectetur blanditiis maxime, eaque inventore ut. Aut facere, quae architecto unde, dolores autem est ratione voluptatum.</p>
    </div>
    <div class="linkTarget" id="section2">
      <h2>Section 2</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorem natus quos voluptatibus tempore repudiandae aut, vero dolorum magni, exercitationem magnam eveniet, omnis soluta doloremque atque iusto provident expedita sint enim.</p>
      <p>Commodi nulla cupiditate rerum culpa at aspernatur dolorem iusto fuga officiis magni, nihil accusamus impedit repellendus obcaecati quod optio, odit reiciendis porro minima explicabo nesciunt earum, facilis quos ut accusantium!</p>
    </div>
    <div class="linkTarget" id="section3">
      <h2>Section 3</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis provident facere molestiae exercitationem eligendi quam perferendis quae enim sed omnis harum rem in officiis veritatis aliquid, corrupti veniam velit deleniti.</p>
      <p>Delectus iusto repellendus quas ipsam! Veritatis a, facilis architecto necessitatibus consequuntur omnis neque aliquid quam exercitationem laboriosam quos magni error numquam enim temporibus, iure cumque modi ipsam soluta dignissimos. Nam!</p>
      <p>Nobis consequatur, esse repudiandae soluta excepturi maiores a in quisquam est natus molestias aspernatur dignissimos, asperiores quasi? Quae nulla mollitia, vero nam eaque incidunt cupiditate consectetur. Repellendus, est dignissimos qui.</p>
    </div>
  </main>
</div>
<div class="footer"></div>
<div id="scroll-to-top"><!--トップへスクロールするボタン-->
  <p>Top</p>
</div>
<script>
//ナビゲーションにリンク項目を追加する関数
const addNavigationItem = () => {
  const sectionsCount = document.querySelectorAll('.linkTarget').length;
  const addedSection = document.querySelectorAll('.linkTarget')[sectionsCount-1];
  const addedSectionTitle = addedSection.querySelector('h2').textContent;
  //li 要素(リンク項目)の生成
  const li = document.createElement('li');
  //innerHTML で子要素(リンク)を追加
  li.innerHTML = `<a href="#section${sectionsCount}">${addedSectionTitle}</a>`;
  //追加先のノードに生成した要素(リンク項目)を追加
  document.getElementById('navigation').appendChild(li);
  //スムーススクロールの関数を実行
  applySmoothScroll(document.querySelectorAll('a[href^="#"]'));
}


/****** MutationObserver の設定 ******/
//MutationObserver のコールバック
const moCallback = (mutationsList) => {
  mutationsList.forEach((mutation) => {
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        for(let node of mutation.addedNodes) {
          // 要素のみを対象とし、他のノード(例 テキストノード)はスキップ
          if (!(node instanceof HTMLElement)) continue;
          // 挿入された要素が linkTarget クラスの div 要素かをチェック
          if (node.matches('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
          // サブツリーのどこかに linkTarget クラスの div 要素がある場合
          for(let elem of node.querySelectorAll('div.linkTarget')) {
            //スクロールスパイの関数を実行
            ioScrollSpy();
            //ナビゲーションにリンク項目を追加する関数を実行
            addNavigationItem();
          }
        }
      }
    }
  });
}
// 監視対象
const moTarget = document.body;
// 監視のオプション
const moConfig = {
  childList: true,
  subtree: true,
};
// コールバック関数を渡してオブザーバーを生成
const mo = new MutationObserver(moCallback);
// 監視対象とオプションを指定して監視を開始
mo.observe(moTarget, moConfig);

//MutationObserver が現在有効かどうか
let isEnabledMO = true;

//チェックボックスのリスナー
document.querySelector('#enableMO').addEventListener('change', (e) => {
  if( e.currentTarget.checked ) {
    //チェックボックスがチェックされれば変更の監視を開始(または再開)
    if(!isEnabledMO) mo.observe(moTarget, moConfig);
    isEnabledMO = true;
  }else{
    //チェックが外されれば変更の監視を停止
    mo.disconnect();
    isEnabledMO = false;
  }
});

//クリックで linkTarget クラスの div 要素を追加するリスナー
document.getElementById('addContent').addEventListener('click', () => {
  let sectionsCount = document.querySelectorAll('.linkTarget').length + 1;
  //div 要素の生成
  const div = document.createElement('div');
  //生成した div 要素にクラス属性を設定
  div.className = 'linkTarget';
  div.id = `section${sectionsCount}`;
  //innerHTML で子要素を追加
  div.innerHTML = `<h2>Section ${sectionsCount}</h2>
  <p>Maiores labore quidem est nemo quia ullam deleniti, ipsum voluptatibus dolorem. Explicabo nisi, possimus. Iusto alias, totam sunt excepturi tempora qui modi autem, ipsum aut doloribus facilis, possimus deserunt atque.</p>
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempora dolores quam a dolorem, architecto molestias fugit necessitatibus, deserunt doloremque! Laborum voluptatum, maiores temporibus ab pariatur reiciendis ipsa ad tenetur in.</p>`;
  //追加先のノードに生成した要素を追加
  document.querySelector('main.contents').appendChild(div);
  //MutationObserver が有効でない場合にはリンク項目も追加
  if(!isEnabledMO) {
    addNavigationItem();
  }
});

/****** スクロールスパイの関数 ******/
const ioScrollSpy = (rootSelector=null, rootMargin='-25% 0px -75%', targetClass='linkTarget', navSelector='#navigation', activeClass='active') => {
  //監視対象の要素を取得
  const targets = document.querySelectorAll('.' + targetClass);
  //オプション
  const options = {
    root: rootSelector ===null ? null : document.querySelector(rootSelector),
    //上から25%の位置を交差の境界とする
    rootMargin: rootMargin,
    threshold: 0 //デフォルトなので省略可能
  }
  //IntersectionObserver のコールバック関数
  const callback = (entries) => {
    // 交差を検知をしたら
    entries.forEach(entry => {
      //監視対象の要素が領域(境界)と交差していれば(isIntersecting が true の場合)
      if (entry.isIntersecting) {
        //その要素を引数として以下の関数を呼び出してメニューをハイライト
        highlightNavMenu(entry.target);
      }
    });
  }
  //IntersectionObserver のインスタンス(オブザーバー)を生成
  const io = new IntersectionObserver(callback, options);
  //それぞれの監視対象の要素を observe() に指定して監視
  targets.forEach( (target) => {
    io.observe(target);
  });
  //メニュー項目をハイライトする(色を変える)関数
  const highlightNavMenu = (target) => {
    //現在アクティブになっている(active クラスが付与されている)メニューの要素を取得
    const highlightedMenu = document.querySelector(navSelector + ' ' + '.' + activeClass);
    //上記で取得した要素が存在すれば、その要素から active クラスを削除
    if (highlightedMenu !== null) {
      highlightedMenu.classList.remove(activeClass);
    }
    //引数で渡された現在交差している(isIntersecting が true の)要素の id 属性からリンク先(href)の値を生成
    const href = '#' + target.id;
    //上記で生成したリンク先を持つメニューが、現在交差している要素のリンク
    const currentActiveMenu =  document.querySelector(`a[href='${href}']`);
    //現在交差している要素のリンクに active クラスを追加
    currentActiveMenu.classList.add(activeClass);
  }
}
//上記で定義した関数を呼び出す
ioScrollSpy();

/****** スムーススクロールの設定 ******/
//アニメーション(スムーススクロール)の持続時間
const duration = 800;
//開始時刻を代入する変数(最初は未定義)
let start;
//スクロール先の Y 座標(ウィンドウの左上からの座標)
let targetY;
//現在の垂直(Y)方向のスクロール量(位置)
let currentY;
//イージング関数の定義
const easeInQuad = (x) => {
  return x * x;
}
//スクロール先を調整する値(上部の固定メニューの高さ)
let offset = 80;
//コールバック関数
const smoothScroll = (timestamp) => {
  if (start === undefined) {
    start = timestamp;
  }
  //経過時間
  const elapsed = start ? timestamp - start : 0;
  //進捗度を算出してイージングを適用
  const relativeProgress =  easeInQuad( Math.min(1, elapsed / duration) );
  //移動する量(targetY)に進捗度を適用して scrollTo のY座標へ指定する値を算出
  const scrollY = currentY + targetY * relativeProgress;
  //上記で算出した位置へスクロール(上部の固定メニューの高さ分を offset で調整)
  window.scrollTo(0, scrollY - offset);
  //進捗度が1未満の場合は自身を繰り返す
  if (relativeProgress < 1) {
    requestAnimationFrame(smoothScroll);
  }
}

//href 属性が # から始まる要素(内部リンク)を全て取得
let links = document.querySelectorAll('a[href^="#"]');
//内部リンクにスムーススクロールを適用される(イベントリスナーを設定する)関数
const applySmoothScroll = (links) => {
  //内部リンクが存在すれば
  if(links.length > 0) {
    //内部リンクのそれぞれの要素に対して以下を実行
    links.forEach((elem) => {
      //それぞれの要素にクリックイベントを設定
      elem.addEventListener('click', (e) => {
        //href 属性の値を取得
        const href = e.currentTarget.getAttribute('href');
        //href 属性の値からスクロール先の要素を取得
        const target = href === "#" ?
          //href 属性の値が # の場合は対象を html 要素
          document.querySelector('html') :
          //それ以外は # 以降の部分を ID として対象の要素
          document.getElementById(href.replace('#', ''));

        //取得した要素が実際に存在すれば以下を実行
        if(target) {
          //開始時刻を初期化
          start = undefined;
          //対象(スクロール先)の要素の Y 座標(ウィンドウ座標)
          targetY =  target.getBoundingClientRect().y;
          //現在の垂直方向にスクロールされている量
          currentY = window.scrollY;
          //関数を実行
          smoothScroll();
        }
      });
    });
  }
}
//上記で定義した関数を呼び出す
applySmoothScroll (links);

//ページトップへスクロールするボタンのクリックイベント
document.getElementById('scroll-to-top').addEventListener('click', (e) => {
  //開始時刻を初期化
  start = undefined;
  //スクロール先の要素を html として Y 座標(ウィンドウ座標)を取得
  targetY =  document.querySelector('html').getBoundingClientRect().y;
  //垂直方向にスクロールされている量(位置)
  currentY  = window.scrollY;
  //関数を実行
  smoothScroll();
});
</script>
</body>
</html>

関連ページ

slotchange イベント

Web Components の slot 要素では、スロットのノードが変更(追加・削除)されると slotchange イベントが発生して変更をイベントとして検知できますが、スロットに入っているノードの子ノード(テキストなど)が変更された場合は、slotchange イベントは発生しません。

MutationObserver を使うとスロットの追加・削除以外の変更も検知することができます。

以下はカスタム要素 custom-menu の定義で、slotchange イベントのリスナーを設定してスロットに変更があると、コンソールにイベントが発生したスロットの name 属性を出力する例です。

// カスタム要素 custom-menu を定義
customElements.define('custom-menu', class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
    <div class="menu">
      <slot name="title"></slot>
      <ul>
        <slot name="item"></slot>
      </ul>
    </div>`;

    // class="menu" の div 要素に slotchange イベントのリスナーを設定
    this.shadowRoot.querySelector('div.menu').addEventListener('slotchange',e => {
      console.log("slotchange Event: " + e.target.name)
    })
  }
});

//カスタム要素を取得
const menu = document.querySelector('custom-menu');

// 1秒後にスロットに要素の追加とテキストの変更を実行
setTimeout(() => {
  console.log('1秒後')
  // li 要素を生成
  const item = document.createElement('li');
  // slot 属性を指定
  item.setAttribute('slot', 'item');
  item.textContent = 'Item 3';
  // カスタム要素に li 要素を追加
  menu.appendChild(item);
  // カスタム要素の slot 属性が title の要素のテキストを変更
  menu.querySelector('[slot="title"]').innerHTML = "New Menu";
}, 1000);
カスタム要素を利用する HTML
<custom-menu>
  <h3 slot="title">Menu</h3>
  <li slot="item">Item 1</li>
  <li slot="item">Item 2</li>
</custom-menu>

以下がコンソールへの出力です。

slotchange Event: title //初期化の際の出力
slotchange Event: item //初期化の際の出力
1秒後
slotchange Event: item  //li 要素の追加による出力

上記では、setTimeout() を使って1秒後にスロットの li 要素を追加し、スロットの h3 要素のテキストを変更していますが、h3 要素のテキストの変更は検知できません(最初の2つの出力は、初期化の際に出力されたもので、変更によるものではありません)。

以下は slotchange イベントの代わりに、MutationObserver を使ってスロットの変更を検知する例です。カスタム要素の定義とスロットの変更部分は前述の例と同じです。

observe() メソッドの第一引数には監視の対象としてカスタム要素 custom-menu を指定し、第二引数のオプションには childList: true と subtree: true を指定しています。

コールバック関数では、type が childList(対象ノードの子ノードの変更)の場合に、addedNodes と removedNodes に含まれるノードを調べて内容をコンソールに出力しています。

customElements.define('custom-menu', class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
    <div class="menu">
      <slot name="title"></slot>
      <ul>
        <slot name="item"></slot>
      </ul>
    </div>
    `;
    //MutationObserver を使ってスロットの変更を検知して、内容をコンソールに出力
    const mo = new MutationObserver((mutations) => {
      for(const mutation of mutations) {
        if ( mutation.type == 'childList' ) {
          if (mutation.addedNodes.length >= 1) {
            mutation.addedNodes.forEach((node) => {
              console.log('追加されたノード: ' + node.nodeName);
              if(node.nodeType ===1 && node.textContent) {
                console.log('テキスト: ' + node.textContent);
              }
              if(node.nodeType === 3) {
                console.log('テキスト: ' + node.data);
                console.log('親ノード: ' + node.parentNode.nodeName);
              }
            })
          }
          if (mutation.removedNodes.length >= 1) {
            mutation.removedNodes.forEach((node) => {
              console.log('削除されたノード: ' + node.nodeName);
              if(node.nodeType ===1 && node.textContent) {
                console.log('テキスト: ' + node.textContent);
              }
              if(node.nodeType === 3) {
                console.log('テキスト: ' + node.data);
              }
            })
          }
        }
      }
    }).observe(document.querySelector('custom-menu'), {
      subtree: true,
      childList: true
    });
  }
});

//以下は前述の例と同じ
const menu = document.querySelector('custom-menu');
setTimeout(() => {
  console.log('1秒後')
  const item = document.createElement('li');
  item.setAttribute('slot', 'item');
  item.textContent = 'Item 3';
  menu.appendChild(item);
  menu.querySelector('[slot="title"]').innerHTML = "New Menu";
}, 1000);

この場合は、テキストの変更も検知され、以下のように出力されます。

1秒後
追加されたノード: LI
テキスト: Item 3
追加されたノード: #text
テキスト: New Menu
親ノード: H3
削除されたノード: #text
テキスト: Menu

通常の MutationObserver 同様、監視オプションに attributes: true や characterData: true を指定すれば、属性の変更やテキストデータの変更も検知することができます。

上記では MutationObserver をカスタム要素のコンストラクター内で定義していますが、コンストラクターの外で定義しても問題ないようです。

関連ページ:Web components の使い方