かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 複数の要素をまとめて追加したい

例えばTODOリストのデータの初期化などループで要素を追加するような処理では、DOMへの要素の追加はレンダリングコストが高いので、なるべくまとめて行いたいです。

jQuery でイベントをもつ複数の要素を丸っとDOMに追加する

jQuery時代は文字列結合でひとまとめにしたものをDOMに追加して、イベントは document.on でイベントを発火させるターゲットを指定していれば、後から追加したテキストでもイベントを発火させることができました。

const data = [
  {text: 'TASK 1', status: 0},
  {text: 'TASK 2', status: 1},
  //...
];

const initTodoList = (data) => {
  const todoList = document.getElementById('#todoList');
  const listText = data.reduce((html, item) => {
    const className = item.status? 'complete' : 'incomplete';
    return html += `<li class="${className}">
      <label>${item.text}</label>
      <button class="doneBtn">DONE</button>
    </li>`;
  }, '');
  todoList.inerHTML = listText;
};

document.on('click.completeItem', '.doneBtn', onComplete);
initTodoList();

JavaScript (VanillaJS) でイベントをもつ複数の要素を丸っとDOMに追加する

jQueryを使わない場合は document.addEvetListener で取れるイベントから event.target を遡って該当するイベントを発火させる事もできますが、少し処理が複雑になってしまいます。
各要素に addEventListener でイベントを付けると、テキスト化してDOMに追加することができませんので、DOMElmentのまま追加するには appendChild() を使いますが、これはリストなど複数の要素を渡すことができません。( jQuery こういう時便利でしたね… )

アンチパターン

ループ内で都度 DOM に追加するとレンダリングコストが高くなる

const data = [...];

const createItem = ({text, status}) => {
  const item = document.createElement('li');
  const doneBtn = document.createElement('button');

  item.className = status? 'complete' : 'incomplete';
  item.innerHTML = `<label>${text}</label>`;

  doneBtn.className = 'doneBtn';
  doneBtn.textContent = 'DONE';
  doneBtn.addEventListener('click', onComplete);

  item.appendChild(doneBtn);
  return item;
};

const initTodoList = (data) => {
  const todoList = document.getElementById('#todoList');
  const listItems = data.map((item) => {
    const itemDOM = createItem(item);
    // 都度追加するのはレンダリングコストが高い
    todoList.appendChild(listItems);
    return itemDOM;
  });

  // これはエラーになるのでループ内で都度DOMに追加する必要がある
  // todoList.appendChild(listItems);
  // => TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
};
initTodoList();

document.createDocumentFragment を使う

Document.createDocumentFragment()
DocumentFragment は DOM ノードです。メインの DOM ツリーの一部にはなりません。通常の使い方は、文書フラグメントを生成し、その文書フラグメントに要素を追加して、その文書フラグメントを DOM ツリーへ追加します。 DOM ツリー内では、文書フラグメントはすべての子要素によって置き換えられます。

文書フラグメントはメモリ内にあり、メインの DOM ツリーの一部ではないため、文書フラグメントに子要素を追加してもページのリフロー (要素の位置と大きさを決定するための計算) が行われません。そのため文書フラグメントを利用することによって、パフォーマンスの改善が見込まれます。
cf. Document.createDocumentFragment() - Web API | MDN

DocumentFragment は後でまるっと追加したい要素を溜めておける透明な袋みたいなものっぽい。

const initTodoList = (data) => {
  const fragment = document.createDocumentFragment();
  data.map((item) => {
    const itemDOM = createItem(item);
    fragment.appendChild(itemDOM);
  });
  document.getElementById('#todoList')
    .appendChild(fragment);
};

DocumentFragment は React で言う所の <></> のようなものみたいで、描画されるDOMに追加しても、描画されず DocumentFragment 内に溜められていた要素が丸っと直接 appendChild するDOM直下に追加され描画されました!

所感

createDocumentFragment めっちゃ便利〜って調べたら結構昔からあった機能なのですね。 jQuery時代の後はReactとか触っててVanilla JSでゴリゴリやる機会も無かったので全く知らなかったです… むしろこれがあったからこそ React の <Fragment></Fragment>/<></> ができたって訳だったのか〜


[参考]

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

FRAGMENT (Special Edition)

FRAGMENT (Special Edition)