Вам не нужно копаться в исходном коде реакции или в коде других реализаций виртуальной DOM. Вам нужно знать только две вещи, чтобы построить свой собственный виртуальный DOM. Хотя виртуальная DOM-система огромна и сложна, на самом деле для основной части виртуального DOM требуется всего 50 строк кода.
Запомните следующие два понятия:
- Виртуальный DOM — это представление любой формы DOM.
- При изменении виртуального дерева DOM вы получаете новое виртуальное дерево. Алгоритм сравнивает различия между двумя деревьями и вносит небольшие необходимые изменения в DOM, чтобы он мог отобразить виртуальный DOM.
визуализировать DOM-дерево
Во-первых, дерево DOM нужно хранить в памяти. Пример представлен обычным объектом JS, предполагающим это дерево DOM:
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
part 1
Как представить это только с помощью чистых JS-объектов? ?
{ type: 'ul',
props: { 'class': 'list'},
children:
[
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
]
}
Два момента, на которые следует обратить внимание:
- Используйте объекты для представления элементов DOM следующим образом.
{ type: '…', props: { … }, children: [ … ] }
- Представляет текстовые узлы DOM с помощью простых строк объектов JS.
part 2
Но написать DOM-дерево таким образом довольно сложно. Итак, вам нужно написать метод, упрощающий построение дерева DOM:
function h(type, props, …children) {
return { type, props, children };
}
Затем мы строим DOM-дерево следующим образом:
h('ul', { 'class': 'list' },
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
);
Делает ли это код более понятным? Далее следуют дальнейшие исследования. Все знакомы с JSX, так как же он работает?
part 3
Согласно официальной документации Babel JSX, Babel переводит код части 1 в часть 2.
// 部分1
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
React.createElement('ul', { className: 'list' },
React.createElement('li', {}, 'item 1'),
React.createElement('li', {}, 'item 2'),
);
какие! Заметили сходство? если мы используемh(…)
вместо этого вызовите функциюReact.createElement(…)
Вызов функции аналогичен. Оказывается, мы можем реализовать эксплойт под названиемjsx
метод компиляции для достижения. нам просто нужнозаголовок исходного файла Включать Исправлены закомментированные строки:
/** @jsx h */
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
Вот такой код, говорит он Бабелю: Привет, будетReact.createElement
вместо этого вызовите функциюh
функция для перевода этого файла jsx. Вы можете поставить любую функцию вместоh
, то код переводится соответствующим образом.
part 4
Итак, подводя итог вышесказанному, напишем DOM следующим образом:
/** @jsx h */
const a = (
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
Таким образом, Babel разберет этот код следующим образом:
const a = (
h('ul', { 'class': 'list' },
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
);
);
когда функцияh
При выполнении он вернет простые объекты JS — представление нашего виртуального DOM:
const a = (
{ type: 'ul',
props: { 'class': 'list'},
children:
[
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
]
}
);
Попробуйте этот код (не забудьте установить Babel):
/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
const a = (
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
console.log(a);
Результат выполнения следующий:
Применить представление DOM
Теперь мы представили дерево DOM в виде простых объектов JS со своей собственной структурой. Разве это не круто!Но нам нужно создать объект DOM на его основе. Потому что мы не можем добавить наше представление к этому простому JS-объекту.
Во-первых, давайте сделаем некоторые предположения и установим некоторую терминологию:
- я буду использовать
$
Начните с записи всех переменных, которые имеют фактические узлы DOM (элементы, текстовые узлы).$parent
Будет элементом DOM. - Представление виртуального DOM будет в переменной с именем node.
- Как и в React, может быть только один корневой узел — все остальные узлы находятся внутри корневого узла.
написать функциюcreateElement(…)
, эта функция принимает виртуальный DOM и возвращает узел DOM. Игнорировать покаprops
and children
, и установите его позже.
function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
return document.createElement(node.type);
}
Потому что могут быть как текстовые узлы (т. е. простые строки JS), так и элементы (т. е. объекты JS, например:{ type: ‘…’, props: { … }, children: [ … ] }
)
Итак, здесь мы можем передать как фиктивный текстовый узел, так и фиктивный узел элемента — и все.
Теперь давайте рассмотрим дочерние свойства, которые являются либо текстовыми узлами, либо узлами элементов. Поэтому они также могут использовать функциюcreateElement(…)
созданный. Подобно рекурсии, поэтому может вызываться для каждого дочернего элементаcreateElement(…)
функцию, затем используйтеappendChild()
чтобы поместить их в наш элемент:
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
какие! Выглядит круто~. Давайте не будем обсуждать это сейчасprops
Node, который будет представлен позже.
Фактическая работа кода:
/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
const a = (
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
const $root = document.getElementById('root');
$root.appendChild(createElement(a));
Vue, React, nodejs, основы JS, оптимизация производительности интерфейса
Изменения процесса
Теперь, когда мы можем преобразовать ваш виртуальный DOM в DOM, пришло время дифференцировать дерево виртуального DOM. Итак, нам нужно написать алгоритм, который будет сравнивать два виртуальных дерева — старое и новое — и вносить в настоящий DOM только необходимые изменения.
Как отличить разные результаты дерева? См. следующие примеры:
- Старых узлов местами нет ----- значит нужно добавить такие узлы, нужно использовать
appendChild(…)
, Как показано ниже - У некоторых нет новых узлов — поэтому эти узлы удаляются и их нужно эксплуатировать.
removeChild(…)
,Как показано ниже - В некоторых местах есть разные узлы, поэтому эти узлы изменены и нужно использовать
replaceChild(…)
,Как показано ниже - Некоторые узлы одинаковы, поэтому вам нужно посмотреть на следующий уровень, чтобы сравнить различия между дочерними узлами.
Таким образом, мы можем написать функцию с именемupdateElement(…)
функция, она имеет три параметра ---$parent
,newNode
,oldNode
. в$parent
является родительским элементом DOM виртуального узла. Вот как мы справляемся со всем вышеперечисленным.
Vue, React, nodejs, основы JS, оптимизация производительности интерфейса
нет старых узлов
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}
нет новых узлов
Есть проблема - если в текущей позиции нового виртуального дерева нет узла - мы должны удалить его из фактического DOM - но что делать? Да, мы знаем родительский элемент, поэтому мы должны вызвать$parent.removeChild(…)
и передайте туда ссылку на элемент DOM. Но мы не называем это так. Если мы знаем, где узел находится в родительском элементе, мы можем использовать$parent.childNodes[index]
чтобы получить ссылку на него. где индекс — это позиция узла в родительском элементе. Хорошо, предположим, что этот индекс будет передан нашей функции, поэтому код должен быть
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
}
}
узел изменен
Во-первых, вам нужно написать функцию для сравнения двух узлов и возврата, действительно ли узел изменился. Мы должны рассматривать его либо как элемент, либо как текстовый узел.
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === ‘string’ && node1 !== node2 ||
node1.type !== node2.type
}
Теперь у нас есть индекс текущего узла в родительском узле, поэтому его легко заменить вновь созданным узлом.
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}
Сравните дочерние узлы
И последнее, но не менее важное: мы должны выполнить итерацию по каждому дочернему элементу обоих узлов и сравнить их — фактически вызывая каждый узел.updateElement(…)
. Да, снова рекурсия.
Но есть несколько вещей, которые следует учитывать перед написанием кода:
- Дочерние элементы следует сравнивать только в том случае, если узел является элементом (текстовые узлы не могут иметь дочерних элементов).
- Теперь передайте ссылку в качестве родительского узла текущему узлу.
- Все дочерние элементы должны сравниваться один за другим — даже если в некоторых случаях появится «undefined» — это нормально — наша функция с этим справится.
- Ну наконец то
index
, он находится вchildren
дочерние узлы массива.
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
поставить все это вместе
Собери весь код вместе, эй! 50 строк кода, чтобы сделать это!
/** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === 'string' && node1 !== node2 ||
node1.type !== node2.type
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
// ---------------------------------------------------------------------
const a = (
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
);
const b = (
<ul>
<li>item 1</li>
<li>hello!</li>
</ul>
);
const $root = document.getElementById('root');
const $reload = document.getElementById('reload');
updateElement($root, a);
$reload.addEventListener('click', () => {
updateElement($root, b, a);
});
Откройте инструменты разработчика и посмотрите, как меняется приложение при нажатии кнопки «перезагрузка». \
Суммировать
Поздравляем! Мы сделали это. Мы написали виртуальную реализацию DOM. и это сработало. Я надеюсь, что, прочитав эту статью, вы поняли основные концепции того, как должен работать виртуальный DOM, и как React работает под капотом.
Если вы считаете, что эта статья неплохая, давайте сделаем серию из трех строк [поделиться, лайкнуть и посмотреть], чтобы ее увидело больше людей~