Skip to content

Virtual DOM

Sᴛѧʀʟɪɴɢ edited this page Oct 30, 2018 · 7 revisions

Как написать свой Virtual DOM

Основные концепции

  • Virtual DOM это любой вид представления реального DOM (any kind of representation of a real DOM).
  • Когда мы что-то изменяем в нашем виртуальном DOM-дереве, мы получаем новое виртуальное дерево. Алгоритм сравнивает старое и новое деревья, находит их различия и делает лишь необходимые изменения в реальном DOM. Что и отражает слово virtual.

Представление нашего дерева DOM

Итак, для начала нам нужно как-то хранить исходное дерево DOM в памяти. Для примера:

<ul class=”list”>  
  <li>item 1</li>    
  <li>item 2</li>    
</ul>

Для этого подойдут обычные JS-объекты:

{  
 type: ‘ul’,
 props: { ‘class’: ‘list’ },
 children: [
  { type: ‘li’, props: {}, children: [‘item 1] },
  { type: ‘li’, props: {}, children: [‘item 2] }
 ]
}

Здесь мы представляем DOM узлы-элементы как объекты. Будем представлять текстовые DOM-узлы как обычные JS-строки.

Для рассмотрения общего случая нам понадобится вспомогательная функция (helper):

const h = (type, props, ...children) => ({ type, props, children });

С её помощью мы можем переписать код так:

h(
 'ul',
 { 'class' : 'list' },
 h('li', {}, 'item 1'),
 h('li', {}, 'item 2'),
);

Уже лучше, но можно пойти дальше. Как работает JSX? Babel транслирует код (transpile) во что-то такое:

React.createElement(
 ‘ul’,
 { className: ‘list’ },
 React.createElement(‘li’, {}, ‘item 1),
 React.createElement(‘li’, {}, ‘item 2),
);

Что очень похоже на нашу вспомогательную функцию. Чтобы сделать что-то подобное, мы можем использовать jsx pragma. Для этого нужно просто над исходным HTML кодом оставить комментарий вида:

/* @jsx h */

В этом случае мы укажем Babel'ю, что нужно транслировать этот JSX, но вместо React.createElement() использовать нашу вспомогательную функцию h().

Таким образом код

/* @jsx h */
const d = (<div>Hello</div>);

транслируется Babel'ем в код:

const d = h('div', {});

А после выполнения вспомогательной функции получим простые JS объекты - наше представление Virtual DOM:

const d = ({ type: 'div', props: {}, children: [] });

Применение нашего представления DOM

У нас уже есть представление дерева DOM со своей структурой в виде объектов. Теперь нам нужно создать реальный DOM по этой структуре.

Cделаем несколько предположений и установим терминологию:

  • Реальные DOM узлы (текстовые и элементы) будем обозначать со знаком $.
  • Представление Virtual DOM будет располагаться в переменной node.
  • Будет использоваться только один корневой компонент (root), как в React.

Напишем функцию createElement(node), принимающую узел Virtual DOM и возвращающую узел реального DOM:

const createElement = node => (typeof node === ‘string’)
  ? document.createTextNode(node)
  : document.createElement(node.type);

Подумаем теперь о **дочерних элементах **(children). Так как они являются узлами, то их так же можно создать с помощью createElement(node). Получается рекурсия. Затем добавляем их с помощью appendChild. Таким образом к функция примет вид:

const createElement = node => {
 if (typeof node === ‘string’) {
   return document.createTextNode(node);
 }
 const $elem = document.createElement(node.type);
 node.children
  .map(createElement)
  .forEach($elem.appendChild.bind($elem));
 return $elem;
};

Обработка изменений

Сейчас мы можем превращать наш Virtual DOM в реальный DOM. Теперь построим алгоритм сравнения старого и нового виртуальных деревьев, а затем изменим необходимое в реальном DOM.

Могут быть следующие случаи:

  • appendChild() - добавление узла. В старом дереве отсутствует узел, который есть в новом дереве на том же месте.
  • removeChild() - удаление узла. В новом дереве отсутствует узел, который есть в старом дереве на том же месте.
  • replaceChild() - замена узла. Узлы на одном и том же месте в старом и новом деревьях не совпадают.
  • Ничего не делаем. Узлы совпадают. Идём искать дальше.

Clone this wiki locally