В прошлом месяце я опубликовал статьи на темы: что такое DOM, и Shadow DOM и чем они отличаются. Чтобы немного вспомнить, повторим основные понятия. «Объектная Модель Документа» (DOM) - это представление HTML кода страницы в виде Javascript объекта и интерфейс для изменения параметров этого объекта. Shadow DOM можно рассматривать как облегченную (малую) версию DOM. Это тоже объектное представление элементов на странице, но не всей страницы сразу а, изолированные кусочки верстки со своими стилями, что позволяет разбить наш документ на меньшие автономные части, которые можно легко переиспользовать на других страницах сайта (приложения).

Еще один подобный термин, который вы могли встретить - «Virtual DOM». Хотя термин существует уже достаточно давно, он стал более популярен благодаря использованию в среде React. В этой статье я расскажу чем собственно является Virtual DOM, чем он отличается от оригинального DOM и как он используется.

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

Чтобы понять как появился термин virtual DOM, нам надо вернуться к оригинальному DOM. Как я упоминал ранее DOM можно условно поделить на 2 части: объектное представление страницы и, собственно, API, для манипуляций с этим объектом.

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

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

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

  • html
    • head lang=”en”
  • body
    • ul class=”list”
      • li class=”list__item”
        • “List item”

Предположим, что мы хотим поменять контент первого элемента на «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 был создан не для этого…

Когда, в далеком 1998, вышла первая версия спецификации DOM, мы строили страницы и управляли ими совсем по другому. Не было необходимости так часто использовать DOM API для изменения контента страницы, как мы делаем это сейчас.

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

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

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

В этом конкретном примере, разница в производительности между методами незначительная. Однако, с ростом объёмов страницы, становится более важным выбирать и обновлять только то, что нужно

… Но ведь был Virtual DOM!

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

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

Как выглядит Virtual DOM?

Имя «виртуальный DOM” только добавляет мистики на то, что происходит на самом деле. По факту виртуальный дом это обычный javascript объект.

Предлагаю вернуться к DOM дереву, которое мы рассматривали ранее:

  • html
    • head lang=”en”
  • body
    • ul class=”list”
      • li class=”list__item”
        • “List item”

Это же дерево может быть представлено в виде 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
    ]
} // end html

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

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

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

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

Теперь, когда мы видели как выглядит Virtual DOM, резонно задать вопрос:, как это может решить проблемы перфоманса и доступа к содержимому страницы?

Как уже упоминалось ранее, с помощью virtual DOM мы можем выделить конкретные изменения которые мы хотим внести в DOM. Давайте вернемся к нашему примеру с неупорядоченным списком и внесем те же изменения с помощью DOM API.

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

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"
        }
    ]
};

Этот объект используется для создания, так называемого, «дифа»(“diff”) - разницы между настоящим виртуальным DOM, в нашем случае списком (<ul>) и нашим объектом с новыми данными. «Дифы» могут выглядеть примерно так:

const diffs = [
    {
        newNode: { /* new version of list item one */ },
        oldNode: { /* original version of list item one */ },
        index: /* index of element in parent's list of child nodes */
    },
    {
        newNode: { /* list item two */ },
        index: { /* */ }
    }
]

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

Например, мы можем пройтись циклом через все «дифы» и, в зависимости от указанных данных, добавить новый элемент или обновить уже имеющийся.

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

diffs.forEach((diff) => {

    const newElement = document.createElement(diff.newNode.tagName);
    /* Add attributes ... */
    
    if (diff.oldNode) {
        // If there is an old version, replace it with the new version
        domElement.replaceChild(diff.newNode, diff.index);
    } else {
        // If no old version exists, create a new node
        domElement.appendChild(diff.newNode);
    }
})

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

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

Чаще с virtual DOM работают через какой-нибудь фреймворк, вместо того чтобы работать с ним в ручную - как в примере выше.

Такие фреймворки как React или Vue используют внутри себя концепцию виртуального DOM, для оптимизации скорости работы с подлинным DOM страницы. Для примера, наш компонент списка мог бы быть реализован с 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);

Если бы мы захотели обновить наш лист, мы могли бы всего-лишь переписать сам темплейт и, затем, вызвать 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, даже в случае если мы переписываем весь темплейт, применятся только те части которые были измененны. Если мы заглянем в dev-tools, в тот момент, когда происходят изменения — мы увидим конкретный элемент и его части которые были подвержены изменениям.

DOM против виртуального DOM

Подводя итоги: виртуальный DOM это просто инструмент который позволяет нам взаимодействовать с DOM страницы более простым и производительным способом. Это, по сути, простой Javascript объект копирующий оригинальный DOM, но, который мы можем изменять как угодно, столько раз сколько потребуется.

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

Данный текст это мой вольный перевод оригинальной статьи Ire Aderinokun - Understanding the Virtual DOM