Виртуальные DOM-узлы
Что такое виртуальный DOM
Виртуальное DOM-дерево — это структура данных JavaScript, описывающая DOM-дерево. Оно состоит из вложенных виртуальных DOM-узлов, также известных как vnodes.
При первой отрисовке виртуальное DOM-дерево используется в качестве шаблона для создания соответствующего DOM-дерева.
Как правило, виртуальные DOM-деревья воссоздаются при каждой отрисовке, обычно в ответ на обработчики событий или изменения данных. Mithril.js сравнивает текущее vnode-дерево с его предыдущей версией и изменяет только те DOM-элементы, которые отличаются.
Может показаться неэффективным так часто воссоздавать vnodes, но современные движки JavaScript могут создавать сотни тысяч объектов менее чем за миллисекунду. С другой стороны, изменение DOM на несколько порядков затратнее, чем создание vnodes.
По этой причине Mithril.js использует сложный и высокооптимизированный алгоритм сравнения виртуального DOM, чтобы минимизировать количество обновлений DOM. Mithril.js также генерирует тщательно разработанные структуры данных vnode, которые компилируются движками JavaScript для обеспечения производительности доступа к данным, близкой к нативной. Кроме того, Mithril.js активно оптимизирует функцию создания vnodes.
Mithril.js уделяет большое внимание поддержке модели рендеринга, которая воссоздает все виртуальное DOM-дерево при каждой отрисовке, чтобы предоставить декларативный API немедленного режима — стиль рендеринга, значительно упрощающий управление сложностью пользовательского интерфейса.
Чтобы проиллюстрировать важность немедленного режима, рассмотрим DOM API и HTML. DOM API — это императивный API сохраненного режима, который требует: 1. написания точных инструкций для сборки DOM-дерева процедурным способом; и 2. написания других инструкций для обновления этого дерева. Императивная природа DOM API означает, что у вас есть много возможностей для микро-оптимизации вашего кода, но это также увеличивает вероятность внесения ошибок и затрудняет понимание кода.
В отличие от этого, HTML ближе к системе рендеринга в немедленном режиме. С помощью HTML вы можете написать дерево DOM гораздо более естественным и читаемым способом, не беспокоясь о том, что забудете добавить дочерний элемент к родителю, столкнетесь с переполнением стека при рендеринге чрезвычайно глубоких деревьев и т. д.
Виртуальный DOM идет на один шаг дальше, чем HTML, позволяя вам писать динамические деревья DOM без необходимости вручную писать несколько наборов вызовов DOM API для эффективной синхронизации пользовательского интерфейса с произвольными изменениями данных.
Основы
Виртуальные DOM-узлы, или vnodes, — это объекты JavaScript, представляющие DOM-элементы (или их части). Механизм виртуального DOM Mithril.js использует vnode-дерево для создания DOM-дерева.
Vnodes создаются с помощью утилиты m()
hyperscript:
m('div', { id: 'test' }, 'hello');
Hyperscript также может использовать компоненты:
// определить компонент
var ExampleComponent = {
view: function (vnode) {
return m('div', vnode.attrs, ['Hello ', vnode.children]);
},
};
// использовать его
m(ExampleComponent, { style: 'color:red;' }, 'world');
// эквивалентный HTML:
// <div style="color:red;">Hello world</div>
Структура
Виртуальные DOM-узлы, или vnodes, — это объекты JavaScript, представляющие элемент (или части DOM) и имеющие следующие свойства:
Свойство | Тип | Описание |
---|---|---|
tag | String|Object | nodeName DOM-элемента. |
key | String? | Значение, используемое для сопоставления DOM-элемента с соответствующим элементом в массиве данных (ключ). |
attrs | Object? | Хеш-карта DOM-атрибутов, событий, свойств и методов жизненного цикла. |
children | (Array|String|Number|Boolean)? | В большинстве типов vnode свойство children является массивом vnodes. Для текстовых и доверенных HTML vnodes свойство children является либо строкой, числом, либо логическим значением. |
text | (String|Number|Boolean)? | Используется вместо children , если vnode содержит текстовый узел в качестве единственного дочернего элемента. Это сделано для повышения производительности. Компонентные vnodes никогда не используют свойство text , даже если у них есть текстовый узел в качестве единственного дочернего элемента. |
dom | Element? | Указывает на элемент, соответствующий vnode. Это свойство undefined в методе жизненного цикла oninit . Во фрагментах и доверенных HTML vnodes dom указывает на первый элемент в диапазоне. |
domSize | Number? | Устанавливается только во фрагментах и доверенных HTML vnodes, и это undefined во всех других типах vnode. Определяет количество DOM-элементов, которые представляет vnode (начиная с элемента, на который ссылается свойство dom ). |
state | Object? | Объект, который сохраняется между перерисовками. Он предоставляется основным движком при необходимости. В POJO-компонентных vnodes state прототипически наследуется от объекта/класса компонента. В классовых компонентных vnodes это экземпляр класса. В компонентных замыканиях это объект, возвращаемый замыканием. |
events | Object? | Объект, который сохраняется между перерисовками и хранит обработчики событий, чтобы их можно было удалить с помощью DOM API. Свойство events является undefined , если не определены обработчики событий. Это свойство используется только внутри Mithril.js, не используйте и не изменяйте его. |
instance | Object? | Для компонентов — место хранения значения, возвращаемого view . Это свойство используется только внутри Mithril.js, не используйте и не изменяйте его. |
Типы Vnode
Свойство tag
vnode определяет его тип. Существует пять типов vnode:
Тип Vnode | Пример | Описание |
---|---|---|
Элемент | {tag: "div"} | Представляет DOM-элемент. |
Фрагмент | {tag: "[", children: []} | Представляет список DOM-элементов, родительский DOM-элемент которых может также содержать другие элементы, не входящие во фрагмент. При использовании вспомогательной функции m() фрагментные vnodes можно создать, только вкладывая массивы в параметр children функции m() . |
Текст | {tag: "#", children: ""} | Представляет текстовый DOM-узел. |
Доверенный HTML | {tag: "<", children: "<br>"} | Представляет список DOM-элементов из HTML-строки. |
Компонент | {tag: ExampleComponent} | Если tag является объектом JavaScript с методом view , vnode представляет DOM, сгенерированный при рендеринге компонента. |
Все в дереве виртуального DOM является vnode, включая текст. Утилита m()
автоматически нормализует свой аргумент children
и превращает строки в текстовые vnodes, а вложенные массивы — во фрагментные vnodes.
Только имена тегов элементов и компоненты могут быть первым аргументом функции m()
. Другими словами, [
, #
и <
не являются допустимыми аргументами selector
для m()
. Доверенные HTML vnodes можно создать с помощью m.trust()
Мономорфный класс
Модуль mithril/render/vnode
используется Mithril.js для генерации всех vnodes. Это гарантирует, что современные движки JavaScript смогут оптимизировать сравнение виртуального DOM, всегда компилируя vnodes в один и тот же скрытый класс.
При создании библиотек, которые генерируют vnodes, вам следует использовать этот модуль вместо написания "голых" объектов JavaScript, чтобы обеспечить высокий уровень производительности рендеринга.
Избегайте анти-паттернов
Избегайте повторного использования vnodes
Vnodes должны представлять состояние DOM в определенный момент времени. Механизм рендеринга Mithril.js предполагает, что повторно используемый vnode не был изменен, поэтому изменение vnode, использованного в предыдущей отрисовке, приведет к непредсказуемому поведению.
Можно повторно использовать vnodes на месте, чтобы предотвратить сравнение, но предпочтительнее использовать onbeforeupdate
.
Избегайте прямой передачи данных модели через атрибуты
Свойство key
может присутствовать в вашей модели данных таким образом, что будет конфликтовать с логикой ключей Mithril.js, и ваша модель может быть изменяемым экземпляром с методом, имя которого совпадает с хуком жизненного цикла, например, onupdate
или onremove
. Например, модель может использовать свойство key
для представления настраиваемого цветового ключа. Когда это изменяется, это может привести к тому, что компоненты будут получать неверные данные, неожиданно менять позиции или другое неожиданное, нежелательное поведение. Вместо этого передавайте его как атрибут, чтобы Mithril.js не истолковал его неправильно (и чтобы вы все еще могли потенциально изменять его или вызывать методы прототипа на нем позже):
// Модель данных
var users = [
{ id: 1, name: 'John', key: 'red' },
{ id: 2, name: 'Mary', key: 'blue' },
];
// Позже...
users[0].key = 'yellow';
// НЕ РЕКОМЕНДУЕТСЯ
users.map(function (user) {
// Компонент John будет пересоздан
return m(UserComponent, user);
});
// РЕКОМЕНДУЕМЫЙ ВАРИАНТ
users.map(function (user) {
// Ключ специально извлечен: модели данных присвоено собственное свойство
return m(UserComponent, { key: user.id, model: user });
});
Избегайте операторов в методах view
Операторы JavaScript в методах view часто требуют изменения естественной вложенной структуры HTML-дерева, делая код более многословным и менее читаемым.
// ИЗБЕГАЙТЕ
var BadListComponent = {
view: function (vnode) {
var list = [];
for (var i = 0; i < vnode.attrs.items.length; i++) {
list.push(m('li', vnode.attrs.items[i]));
}
return m('ul', list);
},
};
Вместо этого рекомендуется использовать выражения JavaScript, такие как тернарный оператор для условного рендеринга, и методы Array для работы со списками.
// ПРЕДПОЧТИТЕЛЬНЕЕ
var BetterListComponent = {
view: function (vnode) {
return m(
'ul',
vnode.attrs.items.map(function (item) {
return m('li', item);
})
);
},
};