Как работает Virtual DOM ?

Схема работы Virtual DOM в Preact

Virtual DOM (VDOM ака VNode) — это волшебный инструмент ✨ который достаточно сложен в понимании. React, Preact и похожие JS библиотеки используют его в своём “ядре”.

К сожалению, я не смог найти хорошую статью или документацию которая смогла бы объяснить принцип работы VDOM простым языком. Так что, я написал её сам.

Получилась ОЧЕНЬ объёмная статья, за счёт большого количества картинок (не текста, всё в порядке).
Все примеры написаны на Preact, но я думаю, что большинство концепций применимы и к React.

Для лучшего понимания VDOM мы пройдёмся по нескольким сценариям:

  1. Babel и JSX
  2. Создание VNode — элемент VDOM
  3. Работа с компонентами и их дочерними компонентами
  4. Rendering и создания DOM элемента
  5. Re-rendering
  6. Удаление DOM элемента
  7. Замена DOM элемента

Краткое обозначение:

📢 — emoji помечает текст где присутствуют ссылки на исходный код.
Реальный DOM — это обычный DOM.

Приложение

Наше приложение — это простое поле поиска которое фильтрует список городов на основе введенных символов. Оно содержит 2 компонента “FilteredList” и “List”. Компонент List отвечает за рендеринг списка элементов (по умолчанию: “California” и “New York”).

📢 Приложение: http://codepen.io/rajaraodv/pen/BQxmjj

От JSX к DOM

Так как же появляется этот “ДОМ” из нашего кода ?

Всё довольно просто. Компоненты написанные на JSX (HTML и JS) преобразуются в чистый JS с помощью CLI инструмента Babel. После чего функция “h” (hyperscript) в Preact преобразует их в VDOM дерево (так называемый VNode). И наконец, Preact’s VDOM алгоритм создает реальный DOM из VDOM (наше приложение).

Уже начали гуглить ? ;)

Не волнуйтесь, сейчас мы всё разберём!

Кликните на картинку

Перед тем, как попасть в волшебную страну под названием VDOM, давайте узнаем побольше о JSX.

1. Babel и JSX

JSX позволяет нам писать HTML в JavaScript! А также даёт возможность использовать в нём JS (правда в фигурных скобках {}).

Без JSX наши компоненты выглядели бы вот так:

Преобразование JSX дерева в JavaScript

JSX это конечно круто, он помогает написать “представление” DOM, но в конечном счете, нам нужен именно реальный DOM.

Таким образом, нам надо преобразовать наше “представление” в JSON (VDOM, который также дерево), чтобы в дальнейшем использовать его в качестве входных данных для создания DOM. Определенно нам нужна функция для того, чтобы сделать это.

И эта функция — “h” function в Preact или “React.createElement” в React.

h” — одна из первых библиотек позволяющая использовать HTML в JS (VDOM)

Для того, чтобы преобразовать JSX к виду “h” функции нам понадобиться Babel. Он проходит через каждый узел JSX и преобразует его к “h” функции.

JSX -> Preact

В React, Babel преобразует JSX к React.createElement.

JSX -> React

Отправная точка приложения

//Добавление к реальному DOM
render(<FilteredList/>, document.getElementById(‘app’));
//Преобразуется к "h":
render(h(FilteredList), document.getElementById(‘app’));

Результат выполнения “h” функции

“h” функция принимает JSX и возвращает так называемый “VNode” (React “createElement” возвращает ReactElement). Preact’s “VNode” (или React’s “ReactElement”) является простым JS объектом узла DOM с его свойствами и наследниками.

Пример VNode:

{
"nodeName": "",
"attributes": {},
"children": []
}

VNode инпута (input) нашего приложения выглядит следующим образом:

{
"nodeName": "input",
"attributes": {
"type": "text",
"placeholder": "Search",
"onChange": ""
},
"children": []
}
“h” функция не создает целое дерево! Она создаёт простой JS объект для одного узла.
📢 Ссылки:
“h” : https://github.com/developit/preact/blob/master/src/h.js
VNode: https://github.com/developit/preact/blob/master/src/vnode.js
“render”: https://github.com/developit/preact/blob/master/src/render.js
“buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102

А теперь, давайте посмотрим, как работает Virtual DOM.

Virtual DOM алгоритм схема для Preact

На блок-схеме ниже изображено создание, обновление и удаление компонентов в Preact, также на ней можно увидеть вызовы эвентов (events) жизненных циклов, как “componentWillMount” и других.

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

Для лучшего понимания я разбил её на несколько сценариев.

Примечание: жёлтым цветом выделены части жизненного цикла .

Сценарий #1: Создание Приложения

1.1 — Создание VNode (Virtual DOM) для компонента

Выделенный раздел показывает первоначальный цикл который создает VNode для компонента. Обратите внимание, что это не создает VNode для дочерних компонентов.

На картинке ниже показано, что происходит когда наше приложение загружается в первый раз. Preact создаёт VNode с наследниками и атрибутами для основного компонента FilteredList.

(click to zoom)

На выходе получился VNode с родительским узлом “div” и его дочерними узлами “input” и “List”

📢 Большинство событий жизненного цикла можно увидеть здесь:
https://github.com/developit/preact/blob/master/src/vdom/component.js

1.2 — Создаём DOM

На данном этапе мы создаём реальный DOM для родительского узла “div”.

Выделенный цикл показывает создание реального DOM для дочерних компонентов

И на выходе получаем “div”, как показано на картинке ниже:

📢 document.createElement: https://github.com/developit/preact/blob/master/src/dom/recycler.js

1.3 — Повторяем эти действия для всех наследников

Теперь мы создаём DOM для наследников (“input” и “List”).

1.4 —Добавление наследников к родителям

Начинаем с обработки узла “input”. Так как у “input” нет дочерних узлов, то цикл сначала добавит (append) “input” в родительский “div”, а затем начнёт обрабатывать “List”.

И наше приложение теперь выглядит образом:

📢 appendChild: https://github.com/developit/preact/blob/master/src/vdom/diff.js

1.5 Обработка дочерних компонентов

В отличие от “input” в компоненте “List” присутствуют наследники и поэтому будет вызван метод render для создания новых VNodes.

После завершения, цикл вернет VNode, который выглядит как показано ниже:

📢 buildComponentFromVNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L102

1.6 Повторить шаги 1.1 - 1.4 для всех дочерних узлов

Цикл ниже будет повторять вышеуказанные шаги для каждого дочернего узла.

После достижения leaf node, алгоритм добавит его к родителю и повторит эту операцию для всех остальных узлов.

Повторяется до тех пор пока все наследники не будут созданы и добавлены к родительскому узлу

Ниже показано как добавляется каждый узел.

1.7 Завершение обработки

И наконец последний пункт. Здесь мы просто вызываем “componentDidMount” для всех компонентов (дочерних и родительских).

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

Сценарий #2: Удаление Leaf Node

Теперь посмотрим на удаление “leaf node” — узел которые не имеет наследников.

Давайте удалим 2-ой элемент в списке (New York) и разберём что при этом происходит.

2.1 Создание VNodes

После первоначального рендеринга, каждое изменение считается “обновлением”. Цикл обновления работает почти так же как и цикл создания (создает VNodes снова и снова).

Отличие в том, что это приводит к вызову “componentWillReceiveProps”, “shouldComponentUpdate”, и “componentWillUpdate” для каждого компонента.

Кроме того, цикл обновления, не создаёт повторно элементы которые уже присутствуют в DOM.

Цикл обновления компонента
📢 removeNode: https://github.com/developit/preact/blob/master/src/dom/index.js#L9
insertBefore:https://github.com/developit/preact/blob/master/src/vdom/diff.js#L253

2.2 Использование ссылок на реальный DOM, чтобы избежать повторного создания узлов

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

Показана связь каждого компонента с DOM

И когда VNodes создан, каждое его свойство сравнивают с узлами в реальном DOM. Если такой узел уже существует, цикл переходит к следующему узлу.

Цикл сравнения
📢 innerDiffNode: https://github.com/developit/preact/blob/master/src/vdom/diff.js#L185

2.3 Удаления не нужного узла в DOM

На рисунке ниже показана разница DOM vs VNode после создание VNodes (пункт 2.1).

(click to zoom)

Как можно заметить в реальном DOM присутствует “New York”, который был удален из VDOM. Цикл ниже решает эту проблему, также он вызовет “componentDidUpdate” как только всё будет сделано.

Цикл удаления узла из DOM

Сценарий #3: Удаление/Unmounting компонента

Теперь к примеру введем в поиск blabla. Как мы знаем, у нас нету ни одного города который соответствует данному запросу, поэтому приложение не будет рендерить компонент “List”.

Поиск не дал результатов

Удаление компонента похоже на удаление одного узла. За исключением того, что мы удаляем узел, который имеет ссылку на компонент, в этом случае библиотека вызовет “componentWillUnmount”, а затем рекурсивно удалит все дочерние элементы DOM. После того, как они будут удалены из реального DOM, он вызовет метод “componentDidUnmount” (в React такого метода нет).

На картинке ниже показана как “ul” ссылается на компонент “List” .

На схеме ниже показано как происходит процесс удаления/unmounting компонента.


И напоследок

Я надеюсь эта статья дала вам базовое представление о том, как работает Virtual DOM (по крайней мере в PReact).

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

На этом всё! 🙏🏼 👍

Оригинал: https://medium.com/@rajaraodv/the-inner-workings-of-virtual-dom-666ee7ad47cf


Если эта статья оказалась для вас полезной, пожалуйста поставьте ❤️, так она сможет помочь другим разработчикам!