DocumentFragment: что это такое и как с ним (не) бороться

Дисклеймер

Похоже, у меня начинается новая серия статей — немного скучная и сугубо утилитарная. В них будут содержаться разъяснения моментов, которые часто вызывают трудности у моих студентов. Если вы матёрый веб-девелопер, скорее всего, вам будет неинтересно. Если вы ждёте извращений в силе «Пятничного JS», их тут не будет, увы.



Одна из вещей, с пониманием которых у студентов регулярно возникают проблемы — это DocumentFragment. В общем-то, я не могу их за это винить. При внешней простоте он имеет несколько неочевидных и даже контринтуитивных свойств. В данной статье я хочу собрать всё, что необходимо про него знать новичку.

image

Что это такое


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

Как создать


Элементарно.

var fragment = document.createDocumentFragment();


Существуют также другие способы, но о них ниже.

Зачем нужен


Как я уже писал выше — для того, чтобы хранить DOM-элементы. «Но их можно хранить и в обычном диве», — может возразить читатель. Верно, однако у фрагмента есть уникальное свойство, которое делает его лучшим кандидатом на эту роль. Рассмотрим следующий код:

var fragment = document.createDocumentFragment();
var parentDiv = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");

fragment.appendChild(div1);
fragment.appendChild(div2);
//сейчас будет интересно
parentDiv.appendChild(fragment);
console.log(parentDiv.children);


Что нам скажет консоль? Человек, не знакомый с DocumentFragment, может подумать, что у parentDiv'а будет один дочерний элемент — fragment. Но на самом деле у него окажется два дочерних элемента — div1 и div2. Дело в том, что сам фрагмент не является DOM-элементом, он лишь контейнер для DOM-элементов. И когда его передают в качестве аргумента в методы типа appendChild или insertBefore, он не встраивается в DOM-дерево, а вместо этого встраивает туда своё содержимое.

И всё-таки зачем нужен?


Свойство «ведра» — это, конечно, хорошо, но как это пригодится на практике? У DocumentFragment две основных области применения.

1. Хранение кусков HTML, не имеющих общего предка.

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

div.innerHTML = "";
div.appendChild(fragmentWithAllContent);


«Но ведь мы можем просто добавлять элементы в div сразу по мере их создания?» — спросит въедливый читатель. Можем, но так делать не стоит, и вот почему.

2. Улучшение производительности в случае множественных вставок.

Дело в том, что каждый раз, когда мы что-то меняем в активном DOM-дереве, браузеру приходится произвести кучу вычислений. Подробнее об этом можно почитать например, здесь. В этой статье ограничимся упоминанием того, что есть такой страшный зверь — reflow. Когда мы добавляем элемент на страницу, этот зверь просыпается и сжирает кусок процессорного времени. Если мы по очереди добавим сто элементов, зверь проснётся сто раз и сто раз сделает «кусь». Для пользователя это может быть уже вполне ощутимым «подвисанием».

Когда мы добавляем элемент в DocumentFragment, это не вызывает reflow, потому что фрагмент не является (и в принципе не может являться) частью активного DOM-дерева. И самое главное: когда мы вставляем содержимое фрагмента с помощью appendChild или других подобных методов, независимо от того, сколько элементов внутри фрагмента, reflow вызывается только один раз.

Для наглядности я сделал простенький бенчмарк, чтобы читатель мог лично увидеть разницу.

Нюансы


Есть две особенности, из-за которых новички часто испытывают трудности при использовании фрагментов. Первая: как я уже писал выше, фрагмент не является DOM-элементом. Это значит, что у него отсутствуют многие привычным методы и свойства, в частности — innerHTML. Поэтому фрагмент нельзя просто так «заселить» из строки. Как это сделать не просто, будет рассказано ниже.

Вторая особенность: фрагмент при использовании «портится». Точнее — опустошается. Когда мы делаем div.appendChild(fragment), все дочерние элементы фрагмента переносятся в div. А поскольку элемент не может иметь более одного родителя, это означает, что они из фрагмента изымаются! Чтобы избежать этого поведения в случае, когда оно нежелательно, можно использовать cloneNode.

Тег