На прошлой неделе я выступил с докладом о кодировании в прямом эфире на Manchester Web Meetup #4. Во время выступления я построил виртуальный DOM с нуля менее чем за час. Это был самый технически сложный доклад, который я когда-либо произносил.
Видео моего выступления выложено здесь. Этот пост, по сути, представляет собой напечатанную версию моего выступления и призван прояснить дополнительные моменты, которые у меня не было времени упомянуть в докладе. Рекомендую посмотреть видео перед прочтением. Это немного облегчило бы задачу.
Вот github repo и codesandbox для кода, который я написал в докладе.
Дополнительные примечания
- В этой статье все переменные будут добавляться с
$
- когда речь идет о реальных домах, например.$div
,$el
,$app
v
- при обращении к виртуальным домам, например.vDiv
,vEl
,vApp
- Эта статья будет представлена как настоящая беседа с добавлением прогрессивного кода тут и там. Каждый раздел будет иметь ссылку codeandbox, показывающую прогресс.
- Эта статья очень-очень длинная. Вероятно, вам потребуется больше получаса, чтобы прочитать. Убедитесь, что у вас достаточно времени, прежде чем читать. Или подумайте о том, чтобы сначала посмотреть видео.
- Если вы заметите какие-либо ошибки, пожалуйста, не стесняйтесь указывать на них!
Обзор
Справочная информация: что такое виртуальный DOM?
Виртуальные DOM обычно относятся к простым объектам, представляющим настоящие DOM.
Объектная модель документа (DOM) — это программный интерфейс для HTML-документов.
Например, когда вы делаете это:
Вы получите DOM для <div id="app"></div>
на странице. Этот DOM будет иметь некоторый программный интерфейс для управления им. Например:
Чтобы сделать простой объект для представления $app
, мы можем написать что-то вроде этого:
Не упомянул в разговоре
Не существует строгого правила того, как должен выглядеть виртуальный DOM. Вы можете назвать его tagLabel
вместо tagName
или props
вместо attrs
. Как только он представляет DOM, это «виртуальный DOM».
Виртуальный DOM не будет иметь ни одного из этих программных интерфейсов. Это то, что делает их легкими по сравнению с реальными моделями DOM.
Однако имейте в виду, что, поскольку модели DOM являются фундаментальными элементами браузера, большинство браузеров должны были провести для них серьезную оптимизацию. Таким образом, настоящие DOM могут быть не такими медленными, как утверждают многие.
Настраивать
Мы начинаем с создания и перехода в каталог нашего проекта.
Выполним первоначальный коммит.
Затем установите Parcel Bundler — упаковщик с нулевой конфигурацией. Он поддерживает все форматы файлов из коробки. Я всегда выбираю сборщика в разговорах о живом кодировании.
источник/main.js
package.json
Теперь вы можете создать сервер разработки, выполнив:
Переходим по адресу http://localhost:1234, и вы должны увидеть hello world на странице и виртуальный DOM, который мы определили в консоли. Если вы их видите, значит, вы правильно настроены!
createElement (имя тега, параметры)
В большинстве реализаций виртуального DOM эта функция называется createElement
, часто называемой h
. Эти функции просто вернут «виртуальный элемент». Итак, давайте реализуем это.
src/vdom/createElement.js
С деструктурированием объекта мы можем написать это так:
src/vdom/createElement.js
Мы также должны разрешить создавать элементы без каких-либо параметров, поэтому давайте установим некоторые значения по умолчанию для наших параметров.
src/vdom/createElement.js
Вспомните виртуальный DOM, который мы создали ранее:
источник/main.js
Теперь это можно записать так:
источник/main.js
Вернитесь в браузер, и вы должны увидеть тот же виртуальный дом, который мы определили ранее. Давайте добавим изображение под div
из giphy:
источник/main.js
Вернитесь в браузер, и вы должны увидеть обновленный виртуальный DOM.
Не упомянул в разговоре
Литералы объектов (например, { a: 3 }
) автоматически наследуются от Object
. Это означает, что объект, созданный объектными литералами, будет иметь методы, определенные в Object.prototype
, такие как hasOwnProperty
, toString
и т. д.
Мы могли бы сделать наш виртуальный DOM немного «чище», используя Object.create(null)
. Это создаст действительно простой объект, который наследует не Object
, а null
.
src/vdom/createElement.js
рендеринг (vNode)
Рендеринг виртуальных элементов
Теперь у нас есть функция, которая генерирует для нас виртуальный DOM. Далее нам нужен способ перевести наш виртуальный DOM в реальный DOM. Давайте определим render (vNode)
, который будет принимать виртуальный узел и возвращать соответствующий DOM.
источник/vdom/render.js
Приведенный выше код должен говорить сам за себя. Я более чем счастлив объяснить больше, если есть запрос на это.
ElementNode и TextNode
В реальном DOM существует 8 типов узлов. В этой статье мы рассмотрим только два типа:
ElementNode
, например<div>
и<img>
TextNode
, обычный текст
Наша структура виртуальных элементов, { tagName, attrs, children }
, представляет только ElementNode
в DOM. Так что нам нужно некоторое представление и для TextNode
. Мы будем просто использовать String
для представления TextNode
.
Чтобы продемонстрировать это, давайте добавим текст в нашу текущую виртуальную модель DOM.
источник/main.js
Расширение рендеринга для поддержки TextNode
Как я уже упоминал, мы рассматриваем два типа узлов. Текущий render (vNode)
отображает только ElementNode
. Итак, давайте расширим render
, чтобы он также поддерживал рендеринг TextNode
.
Сначала мы переименуем нашу существующую функцию renderElem
, как она и делает. Я также добавлю деструктурирование объектов, чтобы код выглядел лучше.
источник/vdom/render.js
Давайте переопределим render (vNode)
. Нам просто нужно проверить, является ли vNode
String
. Если это так, то мы можем использовать document.createTextNode(string)
для рендеринга textNode
. В противном случае просто позвоните по номеру renderElem(vNode)
.
источник/vdom/render.js
Теперь наша функция render (vNode)
способна отображать два типа виртуальных узлов:
- Виртуальные элементы — созданы с помощью нашей функции
createElement
- Виртуальные тексты — представлены строками
Рендерим наш vApp
!
Теперь давайте попробуем визуализировать наши vApp
и console.log
это!
источник/main.js
Перейдите в браузер, и вы увидите консоль, показывающую DOM для:
монтировать ($ узел, $ цель)
Теперь мы можем создать виртуальный DOM и отобразить его в реальном DOM. Далее нам нужно поместить наш настоящий DOM на страницу.
Давайте сначала создадим точку монтирования для нашего приложения. Я заменю Hello world
на src/index.html
на <div id="app"></div>
.
источник/index.html
Что мы хотим сделать сейчас, так это заменить этот пустой div
нашим отрендеренным $app
. Это очень легко сделать, если мы игнорируем Internet Explorer и Safari. Мы можем просто использовать ChildNode.replaceWith
.
Давайте определим mount ($node, $target)
. Эта функция просто заменит $target
на $node
и вернет $node
.
src/vdom/mount.js
Теперь в нашем main.js просто смонтируйте наш $app
в пустой div.
источник/main.js
Теперь наше приложение будет отображаться на странице, и мы должны увидеть кошку на странице.
Сделаем наше приложение интереснее
Теперь давайте сделаем наше приложение более интересным. Мы обернем наш vApp
в функцию с именем createVApp
. Затем он примет count
, который затем будет использовать vApp
.
источник/main.js
Затем мы будем setInterval
увеличивать счетчик каждую секунду и снова создавать, отображать и монтировать наше приложение на странице.
источник/main.js
Обратите внимание, что я использовал $rootEl
для отслеживания корневого элемента. Чтобы mount
знал, куда монтировать наше новое приложение.
Если мы сейчас вернемся в браузер, то увидим, что счетчик увеличивается на 1 каждую секунду, и все работает отлично!
Теперь у нас есть возможность декларативно создать наше приложение. Приложение отображается предсказуемо, и о нем очень легко рассуждать. Если вы знаете, как все делается в JQuery, вы оцените, насколько чище этот подход.
Однако есть пара проблем с повторным рендерингом всего приложения каждую секунду:
- Реальный DOM намного тяжелее виртуального DOM. Рендеринг всего приложения в реальный DOM может быть дорогим.
- Элементы потеряют свои состояния. Например,
<input>
потеряет фокус при каждом повторном монтировании приложения на страницу. Посмотреть живое демо здесь.
Мы решим эти проблемы в следующем разделе.
diff (старый VTree, новый VTree)
Представьте, что у нас есть функция diff (oldVTree, newVTree)
, которая вычисляет разницу между двумя виртуальными деревьями; вернуть функцию patch
, которая принимает реальную DOM oldVTree
и выполняет соответствующие операции с реальной DOM, чтобы реальная DOM выглядела как newVTree
.
Если у нас есть эта функция diff
, то мы можем просто переписать наш интервал, чтобы он стал следующим:
источник/main.js
Итак, давайте попробуем реализовать это diff (oldVTree, newVTree)
. Начнем с простых случаев:
newVTree
этоundefined
- тогда мы можем просто удалить
$node
, переходящий вpatch
!- Они оба являются TextNode (string)
— если это одна и та же строка, ничего не делать.
— если это не так, замените$node
наrender(newVTree)
. - Один из деревьев — это TextNode, другой — ElementNode
. В этом случае это явно не одно и то же, тогда мы заменим$node
наrender(newVTree)
. oldVTree.tagName !== newVTree.tagName
- мы предполагаем, что в этом случае старое и новое деревья совершенно разные.
- вместо того, чтобы пытаться найти различия между двумя деревьями, мы просто заменим$node
наrender(newVTree)
.
- это предположение также существует в реакции. (источник)
- 2 элемента разных типов будут давать разные деревья.
src/vdom/diff.js
Если код достигает (A)
, это означает следующее:
oldVTree
иnewVTree
являются виртуальными элементами.- У них одинаковые
tagName
. - У них могут быть разные
attrs
иchildren
.
Мы реализуем две функции для работы с атрибутами и дочерними элементами по отдельности, а именно diffAttrs (oldAttrs, newAttrs)
и diffChildren (oldVChildren, newVChildren)
, которые будут отдельно возвращать патч. Как мы знаем, на данный момент мы не собираемся заменять $node
, мы можем безопасно вернуть $node
после применения обоих патчей.
src/vdom/diff.js
diffAttrs (oldAttrs, newAttrs)
Давайте сначала сосредоточимся на diffAttrs
. На самом деле это довольно легко. Мы знаем, что собираемся установить все в newAttrs
. После их установки нам просто нужно просмотреть все ключи в oldAttrs
и убедиться, что они все существуют и в newAttrs
. Если нет, удалите их.
Обратите внимание, как мы создаем патч-оболочку и прокручиваем patches
, чтобы применить его.
diffChildren (старые VChildren, новые VChildren)
С детьми будет немного сложнее. Мы можем рассмотреть три случая:
oldVChildren.length === newVChildren.length
- мы можем сделать
diff(oldVChildren[i], newVChildren[i])
, гдеi
идет от0
кoldVChildren.length
.oldVChildren.length > newVChildren.length
- мы также можем сделать
diff(oldVChildren[i], newVChildren[i])
, гдеi
идет от0
кoldVChildren.length
.
-newVChildren[j]
будетundefined
вместоj >= newVChildren.length
- Но это нормально, потому что наш
diff
может обрабатыватьdiff(vNode, undefined)
!oldVChildren.length < newVChildren.length
- мы также можем сделать
diff(oldVChildren[i], newVChildren[i])
, гдеi
переходит от0
кoldVChildren.length
.
- этот цикл создаст патчи для каждого уже существующего дочернего элемента
- нам просто нужно создать оставшиеся дополнительные дочерние элементы, т. е.newVChildren.slice(oldVChildren.length)
.
В заключение, мы прокручиваем oldVChildren
независимо и вызываем diff(oldVChildren[i], newVChildren[i])
.
Затем мы отобразим дополнительных дочерних элементов (если они есть) и добавим их в файл $node
.
Я думаю, будет немного элегантнее, если мы воспользуемся функцией zip
.
Завершенный diff.js
src/vdom/diff.js
Сделайте наше приложение более сложным
Наше текущее приложение на самом деле не использует всю мощь нашего виртуального DOM. Чтобы показать, насколько мощным является наш виртуальный DOM, давайте усложним наше приложение:
источник/main.js
Теперь наше приложение будет генерировать случайное число n
от 0 до 9 и отображать на странице n
фотографий кошек. Если вы зайдете в инструменты разработчика, вы увидите, как мы «разумно» вставляем и удаляем <img>
в зависимости от n
.
Спасибо
Если вы дочитали до этого места, я хотел бы поблагодарить вас за то, что вы нашли время, чтобы прочитать все это. Это очень-очень долго читать! Пожалуйста, оставьте комментарий, если вы действительно прочитали все это. Люблю тебя!
Первоначально опубликовано на https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05.