На прошлой неделе я выступил с докладом о кодировании в прямом эфире на 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 могут быть не такими медленными, как утверждают многие.

Настраивать

https://codesandbox.io/s/7wqm7pv476?expanddevtools=1

Мы начинаем с создания и перехода в каталог нашего проекта.

Выполним первоначальный коммит.

Затем установите Parcel Bundler — упаковщик с нулевой конфигурацией. Он поддерживает все форматы файлов из коробки. Я всегда выбираю сборщика в разговорах о живом кодировании.

источник/main.js

package.json

Теперь вы можете создать сервер разработки, выполнив:

Переходим по адресу http://localhost:1234, и вы должны увидеть hello world на странице и виртуальный DOM, который мы определили в консоли. Если вы их видите, значит, вы правильно настроены!

createElement (имя тега, параметры)

https://codesandbox.io/s/n9641jyo04?expanddevtools=1

В большинстве реализаций виртуального 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)

https://codesandbox.io/s/pp9wnl5nj0?expanddevtools=1

Рендеринг виртуальных элементов

Теперь у нас есть функция, которая генерирует для нас виртуальный DOM. Далее нам нужен способ перевести наш виртуальный DOM в реальный DOM. Давайте определим render (vNode), который будет принимать виртуальный узел и возвращать соответствующий DOM.

источник/vdom/render.js

Приведенный выше код должен говорить сам за себя. Я более чем счастлив объяснить больше, если есть запрос на это.

ElementNode и TextNode

В реальном DOM существует 8 типов узлов. В этой статье мы рассмотрим только два типа:

  1. ElementNode, например <div> и <img>
  2. 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) способна отображать два типа виртуальных узлов:

  1. Виртуальные элементы — созданы с помощью нашей функции createElement
  2. Виртуальные тексты — представлены строками

Рендерим наш vApp!

Теперь давайте попробуем визуализировать наши vApp и console.log это!

источник/main.js

Перейдите в браузер, и вы увидите консоль, показывающую DOM для:

монтировать ($ узел, $ цель)

https://codesandbox.io/s/vjpk91op47

Теперь мы можем создать виртуальный 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

Теперь наше приложение будет отображаться на странице, и мы должны увидеть кошку на странице.

Сделаем наше приложение интереснее

https://codesandbox.io/s/ox02294zo5

Теперь давайте сделаем наше приложение более интересным. Мы обернем наш vApp в функцию с именем createVApp. Затем он примет count, который затем будет использовать vApp.

источник/main.js

Затем мы будем setInterval увеличивать счетчик каждую секунду и снова создавать, отображать и монтировать наше приложение на странице.

источник/main.js

Обратите внимание, что я использовал $rootEl для отслеживания корневого элемента. Чтобы mount знал, куда монтировать наше новое приложение.

Если мы сейчас вернемся в браузер, то увидим, что счетчик увеличивается на 1 каждую секунду, и все работает отлично!

Теперь у нас есть возможность декларативно создать наше приложение. Приложение отображается предсказуемо, и о нем очень легко рассуждать. Если вы знаете, как все делается в JQuery, вы оцените, насколько чище этот подход.

Однако есть пара проблем с повторным рендерингом всего приложения каждую секунду:

  1. Реальный DOM намного тяжелее виртуального DOM. Рендеринг всего приложения в реальный DOM может быть дорогим.
  2. Элементы потеряют свои состояния. Например, <input> потеряет фокус при каждом повторном монтировании приложения на страницу. Посмотреть живое демо здесь.

Мы решим эти проблемы в следующем разделе.

diff (старый VTree, новый VTree)

https://codesandbox.io/s/0xv007yqnv

Представьте, что у нас есть функция diff (oldVTree, newVTree), которая вычисляет разницу между двумя виртуальными деревьями; вернуть функцию patch, которая принимает реальную DOM oldVTree и выполняет соответствующие операции с реальной DOM, чтобы реальная DOM выглядела как newVTree.

Если у нас есть эта функция diff, то мы можем просто переписать наш интервал, чтобы он стал следующим:

источник/main.js

Итак, давайте попробуем реализовать это diff (oldVTree, newVTree). Начнем с простых случаев:

  1. newVTree это undefined
    - тогда мы можем просто удалить $node, переходящий в patch!
  2. Они оба являются TextNode (string)
    — если это одна и та же строка, ничего не делать.
    — если это не так, замените $node на render(newVTree).
  3. Один из деревьев — это TextNode, другой — ElementNode
    . В этом случае это явно не одно и то же, тогда мы заменим $node на render(newVTree).
  4. oldVTree.tagName !== newVTree.tagName
    - мы предполагаем, что в этом случае старое и новое деревья совершенно разные.
    - вместо того, чтобы пытаться найти различия между двумя деревьями, мы просто заменим $node на render(newVTree).
    - это предположение также существует в реакции. (источник)
    - 2 элемента разных типов будут давать разные деревья.

src/vdom/diff.js

Если код достигает (A), это означает следующее:

  1. oldVTree и newVTree являются виртуальными элементами.
  2. У них одинаковые tagName.
  3. У них могут быть разные 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)

С детьми будет немного сложнее. Мы можем рассмотреть три случая:

  1. oldVChildren.length === newVChildren.length
    - мы можем сделать diff(oldVChildren[i], newVChildren[i]), где i идет от 0 к oldVChildren.length.
  2. oldVChildren.length > newVChildren.length
    - мы также можем сделать diff(oldVChildren[i], newVChildren[i]), где i идет от 0 к oldVChildren.length.
    - newVChildren[j] будет undefined вместо j >= newVChildren.length
    - Но это нормально, потому что наш diff может обрабатывать diff(vNode, undefined)!
  3. 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

Сделайте наше приложение более сложным

https://codesandbox.io/s/mpmo2yy69

Наше текущее приложение на самом деле не использует всю мощь нашего виртуального DOM. Чтобы показать, насколько мощным является наш виртуальный DOM, давайте усложним наше приложение:

источник/main.js

Теперь наше приложение будет генерировать случайное число n от 0 до 9 и отображать на странице n фотографий кошек. Если вы зайдете в инструменты разработчика, вы увидите, как мы «разумно» вставляем и удаляем <img> в зависимости от n.

Спасибо

Если вы дочитали до этого места, я хотел бы поблагодарить вас за то, что вы нашли время, чтобы прочитать все это. Это очень-очень долго читать! Пожалуйста, оставьте комментарий, если вы действительно прочитали все это. Люблю тебя!

Первоначально опубликовано на https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05.