5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お礼の品の組み合わせをMutationObserverで監視する

Last updated at Posted at 2021-12-02

この記事は、トラストバンク Advent Calendar 2021の3日目です。

この度、ふるさとチョイスのトップページ控除金額シミュレーション控除上限額の中でもらえるお礼の品の組み合わせをおすすめする機能(以降、お礼の品の組み合わせ表示と省略)がリリースされました。
忙しい年末のふるさと納税に、ぜひご活用ください!

お礼の品の組み合わせ表示を制作するにあたり、JavaScriptの監視インスタンスであるMutationObserverを利用しました。
複雑なDOM操作をシンプルに実装することができるMutationObserverの活用方法を、お礼の品組み合わせ表示の紹介も交えつつ説明します。

お礼の品の組み合わせ表示の複雑な状態管理

キャプチャ.png

お礼の品の組み合わせ表示とは、控除上限額に合わせてお礼の品を組み合わせ、合計金額と共に表示する機能です。
表示された組み合わせは、入れ替えるボタン削除ボタンなどを用いて自分好みにカスタマイズ。
寄付するリストに追加をクリックすることで、一括でお礼の品を寄付するリストに追加することができます。

制作時に悩まされたのは、お礼の品が入れ替わる条件や動作の多さです。
全ての動作に対して個別に寄付金総額や品数の変更を実装すると、更新漏れやソースの難読化に繋がります。

そこで活用するのがMutationObserverです。

<div id="mutation-wrap"></div>
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
    // ここに寄付金総額と品数の再計算を記述する
});

observer.observe(mutationWrapElement, {
    childList: true
});

#mutation-wrapにDOMが追加されるたび、MutationObserver.observeのcallbackとして指定した関数が実行され、寄付金総額と品数が再計算されます。
再計算を実行する条件が、#mutation-wrapにDOMを追加・削除するである限り、新たにJavaScriptを書く必要はありません。

今回は、親となるDOMの直下に発生する操作のみを監視したいため、オプションにはchildListを追加しました。
他にも指定できるオプションは様々です。

オプション名 内容
subtree 孫を監視対象に含める
attributes 属性値の変更を監視
attributeOldValue 属性値が変更された際、変更前の値を取得する
attributeFilter 監視したい属性値をフィルタリングする
characterData テキストの変更を監視
characterDataOldValue テキストが変更された際、変更前の値を取得する

参考:DOM Living Standard - Interface MutationObserver

MutationObserverを用いる際の注意事項

工夫次第で何にでも活用できるMutationObserverですが、利用にあたって注意すべき点がいくつか存在します。

DOMの追加・削除に伴うパフォーマンスの低下

監視したいDOMを単体で指定することができないため、DOMが操作された分だけcallback関数が実行されます。
Document.createDocumentFragmentElement.innerHTMLなどを用い、なるべく一括で操作を実行するようにします。

// NG
children.forEach(child => {
    // childを追加した分実行される
    obseverWrapper.appendChild(child);
});
children.forEach(child => {
    // childを削除した分実行される
    obseverWrapper.removeChild(child);
});

// OK
const fragment = document.createDocumentFragment();

children.forEach(child => {
    fragment.appendChild(child);
});
// 追加を1回にまとめたので、observeも1回だけ実行される
obseverWrapper.appendChild(fragment);
// 削除を1回にまとめたので、observeも1回だけ実行される
obseverWrapper.innerHTML = '';

参考:DOM Living Standard - Interface DocumentFragment

無限ループに陥る場合がある

callback関数の中にMutationObserver.observeで監視しているDOMに対する処理を記述すると、場合によっては無限ループに陥ります。
MutationObserver.disconnectを用いて、一時的に監視を止めてから処理を実行しましょう。

// NG
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
    // 属性値の変更が監視されているので、無限ループに陥る
    mutationWrapElement.children[0].classList.add('view');
});

observer.observe(mutationWrapElement, {
    childList: true,
    attributes: true
});

// OK
const mutationWrapElement = document.getElementById('mutation-wrap');
const observer = new MutationObserver(() => {
    // 監視を停止
    observer.disconnect();
    // 監視が停止しているので、無限ループを回避できる
    mutationWrapElement.children[0].classList.add('view');
    // 監視を再開
    observer.observe(mutationWrapElement, {
        childList: true,
        attributes: true
    });
});

observer.observe(mutationWrapElement, {
    childList: true,
    attributes: true
});

まとめ

JavaScriptには他にも、交差監視を実現するIntersectionObserverやサイズの変化を監視するResizeObserverなど、多くの監視インスタンスが実装されています。
それぞれをうまく取り入れることで、いつかはフレームワークを利用しなくても、直感的で見やすいソースコードが書ける時代が来るかもしれません。

明日の記事は、@a_wis1056さんです。お楽しみに。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?