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

в 9:18, , рубрики: api, javascript, web-разработка, браузеры, Программирование, теги не читают никого
Дисклеймер

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

Тег <template>

Существует одно место, где можно встретить DocumentFragment, не создавая его через JS. Это — свойство content элемента template.

Тэг <template> придуман специально для того, чтобы хранить куски HTML-кода, но раньше времени не нагружать ими браузер. То, что находится внутри этого тега, не становится частью активного DOM-дерева. В частности (на это новички тоже нередко напарываются), их нельзя найти с помощью querySelector. Элементы, созданные из HTML-кода, находящегося внутри тега <template>, не становятся ему дочерними. Вместо этого JavaScript может получить к ним доступ через свойство content, которое является — сюрприз! — как раз DocumentFragment'ом.

С помощью элемента template можно создать фрагмент из строки:

function createFragmentFromString(str){
    var template = document.createElement("template");
    template.innerHTML = str;
    return template.content;
}

Эпилог

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

Автор: Sirion

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js