История и светлое будущее Virtual DOM
5 мая 2022 г.:::предупреждение
Plug*: Я помогаю разрабатывать ✦ Million.js: Virtual DOM в будущее! 💥🦁✨*
Вступление
Виртуальный DOM изначально был разработан авторами React для ускорения рендеринга декларативного пользовательского интерфейса. Чтобы понять, почему декларативный пользовательский интерфейс изначально был таким медленным, нам сначала нужно понять, как декларативный пользовательский интерфейс создавался в прошлом.
Декларативный интерфейс
Традиционный способ написания декларативного пользовательского интерфейса заключается в изменении свойства innerHTML
элемента. Например, если я хочу добавить элемент <div>
в пользовательский интерфейс, я бы написал следующее:
```машинопись
document.body.innerHTML = '
//
теперь имеет дочерний элементМы можем признать, что innerHTML
позволяет нам декларативно определять пользовательский интерфейс, но это не очень эффективно.
Неэффективность связана с синтаксическим анализом, уничтожением и реконструкцией пользовательского интерфейса каждый раз, когда мы меняем innerHTML
. Когда мы меняем innerHTML
, это происходит в четыре этапа:
- Разберите строку
innerHTML
в дереве узлов DOM.
- Удалите все содержимое элемента
<body>
.
- Вставьте дерево узлов DOM в элемент
<body>
.
- Выполните расчет макета и перерисовку экрана.
Этот процесс чрезвычайно затратен в вычислительном отношении и может привести к значительному замедлению скорости рендеринга.
Императивный пользовательский интерфейс
Итак, как решается эта проблема? Что ж, другой вариант — использовать DOM; этот подход более в 3 раза быстрее, чем метод innerHTML
.
```js
const div = document.createElement('div');
div.textContent = 'Привет, мир!';
документ.тело.appendChild(div);
Тем не менее, мы можем признать, что писать вручную может быть обременительно, особенно когда в пользовательском интерфейсе много интерактивности, поскольку нам нужно обязательно указывать каждый шаг. Гораздо элегантнее писать пользовательский интерфейс декларативно.
TL;DR: авторы React создали виртуальный DOM, чтобы мы могли писать пользовательский интерфейс так, чтобы он быстрее отображался, чем
innerHTML
, и был таким же декларативным.
Понимание виртуального DOM
Чтобы лучше понять, как работает Virtual DOM, давайте рассмотрим процесс, а затем создадим пример.
Виртуальный DOM — это метод рендеринга пользовательского интерфейса. Метод использует дерево объектов JavaScript («виртуальные» узлы), которое имитирует дерево DOM.
```js
//
const div = document.createElement('div');
div.style = 'цвет: красный';
div.textContent = 'Привет, мир!';
<div>
выше имитируется виртуальным узлом в следующем объекте JavaScript:
```js
константа divVNode = {
тип: 'див',
реквизит: {
стиль: 'цвет: красный'
дети: ['Привет, мир!']
Мы можем заметить, что виртуальный узел имеет три свойства:
tag
: сохраняет имя тега элемента в виде строки.
props
: сохраняет свойства и атрибуты элемента как объекта.
children
: хранит виртуальные узлы дочерних элементов элемента в виде массива.
Используя виртуальные узлы, мы можем смоделировать, как выглядит текущий пользовательский интерфейс, и что мы хотим, чтобы он изменился при обновлении пользовательского интерфейса.
Допустим, я хочу изменить текст внутри <div>
с "Hello World!"
на "Hello Universe!"
. Используя DOM, мы можем обязательно внести изменения:
```js
//
const div = document.createElement('div');
div.style = 'цвет: красный';
div.textContent = 'Привет, мир!';
// Переход от "Hello World!" до «Привет, Вселенная!»
div.textContent = 'Привет, Вселенная!';
Но с Virtual DOM я могу просто указать, как выглядит текущий пользовательский интерфейс (старый виртуальный узел) и как я хочу, чтобы он выглядел (новый виртуальный узел).
```js
константа oldVNode = {
тип: 'див',
реквизит: {
стиль: 'цвет: красный'
дети: ['Привет, мир!']
константа newVNode = {
тип: 'див',
реквизит: {
стиль: 'цвет: красный'
дети: ['Привет, Вселенная!']
Однако, чтобы виртуальный DOM фактически применил изменение к пользовательскому интерфейсу, нам нужно рассчитать разницу между старым виртуальным узлом и новым виртуальным узлом.
```javascript
тип: 'див',
реквизит: {
стиль: 'цвет: красный'
- дети: ['Привет, мир!']
- дети: ['Привет, Вселенная!']
Как только мы узнаем разницу, виртуальный DOM может изменить пользовательский интерфейс:
```js
div.replaceChild (новый дочерний элемент, старый дочерний элемент);
Вместо замены всего пользовательского интерфейса виртуальный DOM вносит только необходимые изменения.
Создайте свой собственный виртуальный DOM (часть 2)
В этом упражнении мы будем имитировать ✦ Million.js Virtual DOM API. Наш API будет состоять из трех основных функций: m, createElement и patch.
m(тег, реквизит, дети)
Функция m — это вспомогательная функция, которая создает виртуальные узлы. Виртуальный узел содержит три свойства:
tag
: имя тега виртуального узла в виде строки.
props
: свойства/атрибуты узла как объекта.
children
: дочерние элементы виртуального узла в виде массива.
Ниже приведен пример реализации вспомогательной функции m
:
```js
const m = (тег, реквизит = {}, дети = []) => ({
ярлык,
реквизит,
дети,
Таким образом, создание виртуальных узлов становится менее громоздким.
```js
m('div', { style: 'color: red' }, ['Hello World!']);
создатьЭлемент(vnode)
Функция createElement превращает виртуальный узел в реальный элемент DOM. Это важно, потому что мы будем использовать это в нашей функции patch.
Реализация выглядит следующим образом:
- Вернуть текстовый узел, если виртуальный узел является текстовым.
- Создайте новый узел DOM со свойством
tag
виртуального узла.
- Пройдитесь по реквизитам виртуального узла и добавьте их в узел DOM.
- Перебрать дочерние элементы, рекурсивно вызвать createElement для каждого дочернего элемента и добавить их в узел DOM.
```js
const createElement = (vnode) => {
если (тип vnode === 'строка') {
вернуть документ.createTextNode(vnode);
const el = document.createElement(vnode.tag);
for (const prop в vnode.props) {
эль [реквизит] = vnode.props [реквизит];
for (const дочерний элемент vnode.children) {
el.appendChild (создатьЭлемент (дочерний элемент));
возврат эл;
Таким образом, мы можем легко преобразовать виртуальные узлы в узлы DOM:
```javascript
//
создатьЭлемент(
m('div', {style: 'color: red' }, ['Hello World!'])
patch(el, newVNode, oldVNode)
Функция patch принимает существующий узел DOM, старый виртуальный узел и новый виртуальный узел.
Реализация выглядит следующим образом:
- Рассчитайте разницу между двумя виртуальными узлами.
- Если виртуальный узел является строкой, замените текстовое содержимое узла DOM новым узлом.
- Если виртуальный узел является «объектом», обновите узел, если «тег», «реквизиты» или «дочерние элементы» отличаются.
```js
const patch = (el, newVNode, oldVNode) => {
if (!newVNode && newVNode !== '') return el.remove();
если (
typeof oldVNode === 'строка' ||
typeof newVNode === 'строка'
если (старыйVNode !== новыйVNode) {
return el.replaceWith(createElement(newVNode));
} еще {
если (старыйVNode.tag !== новыйVNode.tag) {
return el.replaceWith(createElement(newVNode));
// исправление реквизита
for (const prop in {
...старыйVNode.props,
...новыйVNode.props,
если (newVNode.props [реквизит] === не определено) {
удалить эл[реквизит];
} иначе если (
oldVNode.props[реквизит] === не определено ||
oldVNode.props[реквизит] !== новыйVNode.props[реквизит]
el[prop] = newVNode.props[prop];
// исправляем дочерние элементы
for (пусть i = oldVNode.children.length - 1; i >= 0; --i) {
пластырь(
эл.дочерние узлы[i],
новыйVNode.children[i],
oldVNode.children[i]
за (
пусть i = oldVNode.children.length;
i < newVNode.children.length;
я++
el.appendChild (createElement (newVNode.children [i]));
Таким образом, мы можем обновить пользовательский интерфейс с помощью функции «заплатки».
```js
const oldVNode = m('div', { стиль: 'цвет: красный' }, [
'Привет, мир!',
const newVNode = m('div', { стиль: 'цвет: красный' }, [
«Привет, Вселенная!»,
const el = createElement(oldVNode);
//
patch(el, oldVNode, newVNode);
//
И мы закончили наш Виртуальный ДОМ! Посмотрите [живой пример здесь] (https://codesandbox.io/s/virtual-dom-example-8nte0o).
Виртуальный DOM — это чистые накладные расходы
«Виртуальный DOM — это чистые накладные расходы» — Рич Харрис, 2018 г.
В настоящее время реализации Virtual DOM несут затраты на вычисления при вычислении различий между старыми и новыми виртуальными узлами.
Даже с чрезвычайно эффективными алгоритмами сравнения (такими как list-diff2
), когда деревья виртуальных узлов становятся больше, чем двузначные числа виртуальных узлов, стоимость сравнения становится значительной.
Известно, что алгоритмы дифференциации деревьев работают медленно. Временная сложность может варьироваться от «O (n)» до «O (n ^ 3)» в зависимости от сложности дерева виртуальных узлов. Это далеко от манипулирования DOM, которое в большинстве случаев составляет «O (1)».
Будущее виртуального DOM
"Компиляторы — это новые фреймворки" -- Том Дейл, 2017 г.
В 2017 году Том Дейл, создатель [Ember] (https://emberjs.com/), был одним из первых фанатиков открытого исходного кода, выступивших за использование компиляторов для библиотек пользовательского интерфейса JavaScript.
В 2022 году мы теперь знаем, что ставка Тома Дейла была верной. В экосистеме JavaScript наблюдается рост "скомпилированных" библиотек, таких как Solid и Svelte, которые отказываются от виртуального DOM. Эти библиотеки пропускают ненужный рендеринг, используя компилятор для предварительного рендеринга и генерируя код только при использовании.
Виртуальный DOM, с другой стороны, отстает от этой тенденции. Текущие библиотеки Virtual DOM по своей сути несовместимы с компилятором «по запросу». В результате скорость рендеринга Virtual DOM часто медленнее, чем у современных библиотек пользовательского интерфейса «без виртуального DOM» на несколько величин.
Если мы хотим, чтобы виртуальный DOM был конкурентоспособным по скорости рендеринга в будущем, нам нужно реконструировать виртуальный DOM, чтобы обеспечить расширение компилятора.
Оригинал