en.javascript.info/2-ui/1-document/12-multi-insert/article.md
2015-01-29 14:04:02 +03:00

16 KiB
Raw Blame History

Мультивставка: insertAdjacentHTML и DocumentFragment

Обычные методы вставки работают с одним узлом. Но есть и способы вставлять множество узлов одновременно. [cut]

Оптимизация вставки в документ

Рассмотрим задачу: сгенерировать список UL/LI.

Есть две возможных последовательности:

  1. Сначала вставить `UL` в документ, а потом добавить к нему `LI`:
    var ul = document.createElement('ul');
    document.body.appendChild(ul); // сначала в документ
    for(...) ul.appendChild(li);   // потом узлы
    
  2. Полностью создать список "вне DOM", а потом -- вставить в документ:
    var ul = document.createElement('ul');
    for(...) ul.appendChild(li);   // сначала вставить узлы
    document.body.appendChild(ul); // затем в документ
    

Как ни странно, между этими последовательностями есть разница. В большинстве браузеров, второй вариант -- быстрее.

Почему же? Иногда говорят: "потому что браузер перерисовывает каждый раз при добавлении элемента". Это не так. Дело вовсе не в перерисовке.

Браузер достаточно "умён", чтобы ничего не перерисовывать понапрасну. В большинстве случаев процессы перерисовки и сопутствующие вычисления будут отложены до окончания работы скрипта, и на тот момент уже совершенно без разницы, в какой последовательности были изменены узлы.

Тем не менее, при вставке узла происходят разные внутренние события и обновления внутренних структур данных, скрытые от наших глаз.

Что именно происходит -- зависит от конкретной, внутренней браузерной реализации DOM, но это отнимает время. Конечно, браузеры развиваются и стараются свести лишние действия к минимуму.

[online]

Бенчмарк [#insert-bench-tbody]

Чтобы легко проверить текущее состояние дел -- вот два бенчмарка.

Оба они создают таблицу 20x20, наполняя TBODY элементами TR/TD.

При этом первый вставляет все в документ тут же, второй -- задерживает вставку TBODY в документ до конца процесса.

Кликните, чтобы запустить.

Код для тестов находится в файле insert-bench.js.

[/online]

Добавление множества узлов

Продолжим работать со вставкой узлов.

Рассмотрим случай, когда в документе уже есть большой список UL. И тут понадобилось срочно добавить еще 20 элементов LI.

Как это сделать?

Если новые элементы пришли в виде строки, то можно попробовать добавить их так:

ul.innerHTML += "<li>1</li><li>2</li>...";

Но операция += с innerHTML не работает с DOM. Она не прибавляет, а заменяет всё содержимое списка на дополненную строку. Это не только медленно, но все внешние ресурсы (картинки) будут загружены заново. Так лучше не делать.

А если нужно вставить в середину списка? Здесь innerHTML вообще не поможет.

Можно, конечно, вставить строку во временный DOM-элемент и перенести оттуда элементы, но есть и гораздо лучший вариант: метод insertAdjacentHTML!

insertAdjacent*

Метод insertAdjacentHTML позволяет вставлять произвольный HTML в любое место документа, в том числе и между узлами!

Он поддерживается всеми браузерами, кроме Firefox меньше версии 8, ну а там его можно эмулировать.

Синтаксис:

elem.insertAdjacentHTML(where, html);
`html`
Строка HTML, которую нужно вставить
`where`
Куда по отношению к `elem` вставлять строку. Всего четыре варианта:
  1. `beforeBegin` -- перед `elem`.
  2. `afterBegin` -- внутрь `elem`, в самое начало.
  3. `beforeEnd` -- внутрь `elem`, в конец.
  4. `afterEnd` -- после `elem`.

Например, вставим пропущенные элементы списка перед <li>5</li>:

<!--+ run -->
<ul>
  <li>1</li>
  <li>2</li>
  <li>5</li>
</ul>

<script>
var ul = document.body.children[0];
var li5 = ul.children[2];

li5.insertAdjacentHTML("beforeBegin", "<li>3</li><li>4</li>");
</script>

Единственный недостаток этого метода -- он не работает в Firefox до версии 8. Но его можно легко добавить, используя полифилл insertAdjacentHTML для Firefox.

У этого метода есть "близнецы-братья", которые поддерживаются везде, кроме Firefox, но в него они добавляются тем же полифиллом:

  • [elem.insertAdjacentElement(where, newElem)](http://help.dottoro.com/ljbreokf.php) -- вставляет в произвольное место не строку HTML, а элемент `newElem`.
  • [elem.insertAdjacentText(where, text)](http://help.dottoro.com/ljrsluxu.php) -- создаёт текстовый узел из строки `text` и вставляет его в указанное место относительно `elem`.

Синтаксис этих методов, за исключением последнего параметра, полностью совпадает с insertAdjacentHTML. Вместе они образуют "универсальный швейцарский нож" для вставки чего угодно куда угодно.

DocumentFragment

[warn header="Важно для старых браузеров"] Оптимизация, о которой здесь идёт речь, важна в первую очередь для старых браузеров, включая IE9-. В современных браузерах эффект от нее, как правило, небольшой, а иногда может быть и отрицательным. [/warn]

До этого мы говорили о вставке строки в DOM. А что делать в случае, когда надо в существующий UL вставить много DOM-элементов?

Можно вставлять их один за другим, вызовом insertBefore/appendChild, но при этом получится много операций с большим живым документом.

Вставить пачку узлов единовременно поможет DocumentFragment. Это особенный кросс-браузерный DOM-объект, который похож на обычный DOM-узел, но им не является.

Синтаксис для его создания:

var fragment = document.createDocumentFragment();

В него можно добавлять другие узлы.

fragment.appendChild(node);

Его можно клонировать:

fragment.cloneNode(true); // клонирование с подэлементами

У DocumentFragment нет обычных свойств DOM-узлов, таких как innerHTML, tagName и т.п. Это не узел.

Его "Фишка" заключается в том, что когда DocumentFragment вставляется в DOM -- то он исчезает, а вместо него вставляются его дети. Это свойство является уникальной особенностью DocumentFragment.

Например, если добавить в него много LI, и потом вызвать ul.appendChild(fragment), то фрагмент растворится, и в DOM вставятся именно LI, причём в том же порядке, в котором были во фрагменте.

Псевдокод:

// хотим вставить в список UL много LI

// делаем вспомогательный DocumentFragment
var fragment = document.createDocumentFragment();

for (цикл по li) {
  fragment.appendChild(list[i]); // вставить каждый LI в DocumentFragment 
}

ul.appendChild(fragment);   // вместо фрагмента вставятся элементы списка

В современных браузерах эффект от такой оптимизации может быть различным, а на небольших документах иногда и отрицательным.

Понять текущее положение вещей вы можете, запустив следующий [edit src="benchmark"]небольшой бенчмарк[/edit].

append/prepend, before/after, replaceWith

Сравнительно недавно в стандарте появились методы, которые позволяют вставить что угодно и куда угодно.

Синтаксис:

  • `node.append(...nodes)` -- вставляет `nodes` в конец `node`,
  • `node.prepend(...nodes)` -- вставляет `nodes` в начало `node`,
  • `node.after(...nodes)` -- вставляет `nodes` после узла `node`,
  • `node.before(...nodes)` -- вставляет `nodes` перед узлом `node`,
  • `node.replaceWith(...nodes)` -- вставляет `nodes` вместо `node`.

Эти методы ничего не возвращают.

Во всех этих методах nodes -- DOM-узлы или строки, в любом сочетании и количестве. Причём строки вставляются именно как текстовые узлы, в отличие от insertAdjacentHTML.

Пример (с полифиллом):

<!--+ run autorun height=80 -->
<html>
<head>
  <meta charset="utf-8">
  <script src="//polyfill.webservices.ft.com/v1/polyfill.js?features=Element.prototype.mutation"></script>
</head>

<body>
<script>
  // добавим элемент в конец <body>
  var p = document.createElement('p');
  document.body.append(p);

  var em = document.createElement('em');
  em.append('Мир!');

  // вставить в параграф текстовый и обычный узлы
  p.append("Привет, ", em);

  // добавить элемент после <p>
  p.after(document.createElement('hr'))
</script>

</body>
</html>

Итого

  • Манипуляции, меняющие структуру DOM (вставка, удаление элементов), как правило, быстрее с отдельным маленьким узлом, чем с большим DOM, который находится в документе.

    Конкретная разница зависит от внутренней реализации DOM в браузере.

  • Семейство методов для вставки HTML/элемента/текста в произвольное место документа:
    • `elem.insertAdjacentHTML(where, html)`
    • `elem.insertAdjacentElement(where, node)`
    • `elem.insertAdjacentText(where, text)`

    Два последних метода не поддерживаются в Firefox, на момент написания текста, но есть небольшой полифилл insertAdjacentFF.js, который добавляет их. Конечно, он нужен только для Firefox.

  • `DocumentFragment` позволяет минимизировать количество вставок в большой живой DOM. Эта оптимизация особо эффективна в старых браузерах, в новых эффект от неё меньше или наоборот отрицательный.

    Элементы сначала вставляются в него, а потом -- он вставляется в DOM. При вставке DocumentFragment "растворяется", и вместо него вставляются содержащиеся в нём узлы.

    DocumentFragment, в отличие от insertAdjacent*, работает с коллекцией DOM-узлов.

  • Современные методы, работают с любым количеством узлов и текста, желателен полифилл:
    • `append/prepend` -- вставка в конец/начало.
    • `before/after` -- вставка после/перед.
    • `replaceWith` -- замена.

[head]

[/head]