Перевод статьи Understanding the Virtual DOM

Автором статьи уже публиковались статьи о DOM и shadow DOM, а так же об их различиях и тем не менее была написана эта статья.

Объектная модель документа (Document Object Model) — это представление HTML документа в виде объекта, кроме того она предоставляет интерфейс для управления этим объектом.

Теневой «shadow DOM» можно рассматривать как «облегченную» версию упомянутой выше модели DOM. Это объектное представление отдельных элементов HTML не являющееся полностью автономным HTML документом. Использование концепции «shadow DOM» позволяет разделить DOM документа на отдельные меньшие по размеру инкапсулированные части, которые затем можно использовать в других HTML документах.

Другой схожий, с выше рассмотренным, термин, с которым вы, вероятно, уже сталкивались, это виртуальный «virtual DOM». И хотя эта концепция существует уже несколько лет, но стала она популярной относительно недавно благодаря ее использованию в фреймворке React. В этой статье я расскажу, что такое виртуальный DOM, чем он отличается от «реального», а также как его применять на практике.

Зачем нам нужен виртуальный DOM?

Для того, что бы понять, почему возникла концепция виртуального DOM, давайте вернемся к представлению DOM обычного HTML документа. И как уже говорилось выше, он включает в себя две составляющие — объектное представление HTML документа и API для управления этим объектом.

Для примера, возьмем простой HTML документ, содержащий неупорядоченный список с одним элементом.

<!doctype html>
<html lang="en">
 <head></head>
 <body>
    <ul class="list">
        <li class="list__item">List item</li>
    </ul>
  </body>
</html>

Этот документ может быть представлен в виде дерева DOM следующего вида:

Допустим мы захотим изменить текстовое содержимое первого элемента списка на следующее “List item one”, а также добавить в список второй элемент с произвольным содержимым. Для этого необходимо использовать API DOM, чтобы найти те элементы которые мы хотим обновить, создать новые элементы, добавить им необходимые атрибуты и содержимое, а затем обновить существующее дерево DOM с учетом изменений.

const listItemOne = document.getElementsByClassName("list__item")[0];
listItemOne.textContent = "List item one";

const list = document.getElementsByClassName("list")[0];
const listItemTwo = document.createElement("li");
listItemTwo.classList.add("list__item");
listItemTwo.textContent = "List item two";
list.appendChild(listItemTwo);

 

DOM не был создан для этого…

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

При этом такие методы как document.getElementsByClassName() хорошо использовать для решения простых задач. Но если мы хотим постоянно обновлять на странице большое количество элементов каждые несколько секунд, то это может стать достаточно дорогой, с точки зрения производительности, операцией: постоянно запрашивать и обновлять элементы DOM.

Таким образом, из-за особенностей реализации API DOM проще (и выгоднее из-за общей производительности выполнения кода) выполнять более «дорогие» операции по обновлению больших частей DOM, чем осуществлять поиск и обновление (изменение) каждого элемента DOM по отдельности.

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

const list = document.getElementsByClassName("list")[0];
list.innerHTML = `
<li class="list__item">List item one</li>
<li class="list__item">List item two</li>
`;

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

… но для этого существует виртуальный DOM

Виртуальный DOM был создан для решения проблем, связанных с необходимостью частого обновления дерева DOM, при чём способом с большей производительностью. В отличие от «обычного» и теневого «shadow DOM», виртуальный DOM не является официальной спецификацией, а представляет собой новый метод взаимодействия с DOM HTML документа.

Виртуальный DOM может рассматриваться как копия исходного DOM (или его части). Этой копией можно манипулировать и обновлять с какой угодно частотой, не используя API DOM нашей страницы. После того как все обновления виртуального DOM были произведены, мы можем решить, какие конкретные изменения необходимо внести в «реальный» DOM, и сделать это единовременно самым оптимальным способом.

Как выглядит виртуальный DOM?

Название виртуальный DOM добавляет немножко магии к загадке, так что же на самом деле представляет собой эта концепция? Фактически, виртуальный DOM — это просто обычный объект Javascript.

Давайте вернемся к дереву DOM, которое мы создали ранее:

Все дерево HTML документа может быть также представлено как литерал объекта Javascript:

const vdom = {
    tagName: "html",
    children: [
        { tagName: "head" },
        {
            tagName: "body",
            children: [
                {
                    tagName: "ul",
                    attributes: { "class": "list" },
                    children: [
                        {
                            tagName: "li",
                            attributes: { "class": "list__item" },
                            textContent: "List item"
                        } // end li

                    ]
                } // end ul

            ]
        } // end body

    ]
}

Мы также можем рассматривать этот объект как виртуальный DOM. Как и исходный DOM, это объектное представление нашего HTML документа. Но так как это простой объект Javascript, мы можем свободно и как угодно часто манипулировать его содержимым, не касаясь реального DOM до тех пор пока нам это не понадобится.

Вместо того, чтобы использовать единственный объект для представления всего содержимого HTML документа полностью, более распространенной практикой является работа с отдельными частями документа с использованием концепции виртуального DOM. Например, мы можем работать с отдельным компонентом list, который будет непосредственно привязан к элементу (узлу) нашего неупорядоченного списка ul.

const list = {
    tagName: "ul",
    attributes: { "class": "list" },
    children: [
        {
            tagName: "li",
            attributes: { "class": "list__item" },
            textContent: "List item"
        }
    ]
};

 

Под капотом виртуального DOM

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

Как я уже упоминалось, мы можем использовать формат виртуального DOM для отделения элементов, содержимое которых необходимо изменять в «реальном» DOM, а затем изолированно сколь угодно изменять их.

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

const copy = {
    tagName: "ul",
    attributes: { "class": "list" },
    children: [
        {
            tagName: "li",
            attributes: { "class": "list__item" },
            textContent: "List item one"
        },
        {
            tagName: "li",
            attributes: { "class": "list__item" },
            textContent: "List item two"
        }
    ]
};

Этот объект copy используется для создания того, что называется diff (разницы) между исходным и измененным состоянием, в данном случае исходным компонентомlist, и его измененным представлением в виртуальном DOM. Объект diff может выглядеть примерно так:

const diff = [
    {
        newNode: { /* новая версия элемента списка */ 
        },
        oldNode: { /* исходная версия элемента списка */ 
        },
        index: { /* индекс элемента в родительском списке */
    },
    {
        newNode: { /* второй элемент списка */ 
        },
        index: {      }
    }
]

Полученный выше объектdiff представляет собой ни что иное как инструкцию по обновлению исходного дерева DOM. Как только все различия собраны вместе, мы можем пакетно вносить изменения в DOM нашей страницы, делая только необходимые обновления содержимого ее отдельных элементов.

Затем мы можем перебрать каждое изменение из объекта diff в цикле, то есть добавим элементу список ul нового потомка li или же обновим содержимое старого в зависимости от того, что будет указано в diff.

const domElement = document.getElementsByClassName("list")[0];

diff.forEach((diff) => {

    const newElement = document.createElement(diff.newNode.tagName);
    /* добавим атрибуты ... */
    
    if (diff.oldNode) { // Если есть старая версия заменить ее новой
        domElement.replaceChild(diff.newNode, diff.index);
    } else { // Если нет старой версии, создать новый элемент node
        domElement.appendChild(diff.newNode);
    }
})

Обратите внимание, что это упрощенная и сильно урезанная версия того, как может работать виртуальный DOM и есть множество нюансов, которые мы не рассматриваем в этой статье.

Виртуальный DOM и фреймворки

Наиболее часто концепция виртуального DOM применяется в фреймворках. При этом не используя эту концепцию явно, как это показано в примере выше. Рассмотрим как React используют концепцию виртуального DOM для реализации более производительных обновлений дерева DOM. Например, наш элемент списка list как компонент React может быть представлен следующим образом.

import React from 'react';
import ReactDOM from 'react-dom';

const list = React.createElement("ul", { className: "list" },
    React.createElement("li", { className: "list__item" }, "List item")
);

ReactDOM.render(list, document.body);

Если бы мы захотели обновить содержимое нашего списка, то мы просто изменили бы шаблон (template) списка и снова вызвали метод ReactDOM.render(), передав ему в качестве аргумента новый список.

const newList = React.createElement("ul", { className: "list" },
    React.createElement("li", { className: "list__item" }, "List item one"),
    React.createElement("li", { className: "list__item" }, "List item two");
);

setTimeout(() => ReactDOM.render(newList, document.body), 5000);

Поскольку React использует виртуальный DOM, то даже если мы перерисовываем всю страницу, обновляются только те части, которые действительно необходимо изменить. Если мы посмотрим на это в действии с использованием инструментов разработчика (developer tools) браузера, то увидим следующее. Когда производятся изменения дерева Html документа, то мы увидим что обновляются лишь его отдельные элементы или части страницы, не изменяя всего ее содержимого.

 

DOM vs. виртуальный DOM

Напомним, что виртуальный DOM — это инструмент, который позволяет нам взаимодействовать с элементами DOM более простым и производительным способом. Это представление DOM в виде Javascript объекта, который мы можем изменять так часто, как нам нужно. Изменения, внесенные в этот объект, сопоставляются с прежней версией исходного DOM страницы или ее части. А затем изменяются только те ее отдельные части, которые необходимо обновить. Таким образом, обращение к отдельным элементам страницы, а также их изменение в «реальном» DOM производятся реже, что непосредственно влияет на конечную производительность нашего кода.

Оставить комментарий