Virtual dom react что это
Dec 28, 2016 · 8 min read
Virtual DOM (VDOM ака VNode) — это волшебный инструмент ✨ который достаточно сложен в понимании. React, Preact и похожие JS библиотеки используют его в своём “ядре”.
К сожалению, я не смог найти хорошую статью или документацию которая смогла бы объяснить принцип работы VDOM простым языком. Так что, я написал её сам.
Получилась ОЧЕНЬ объёмная статья, за счёт большого количества картинок (не текста, всё в порядке).
Все примеры написаны на Preact, но я думаю, что большинство концепций применимы и к React.
Для лучшего понимания VDOM мы пройдёмся по нескольким сценариям:
Краткое обозначение:
📢 — emoji помечает текст где присутствуют ссылки на исходный код.
Реальный DOM — это обычный DOM.
Приложение
Наше приложение — это простое поле поиска которое фильтрует список городов на основе введенных символов. Оно содержит 2 компонента “FilteredList” и “List”. Компонент List отвечает за рендеринг списка элементов (по умолчанию: “California” и “New York”).
От JSX к DOM
Всё довольно просто. Компоненты написанные на JSX (HTML и JS) преобразуются в чистый JS с помощью CLI инструмента Babel. После чего функция “h” (hyperscript) в Preact преобразует их в VDOM дерево (так называемый VNode). И наконец, Preact’s VDOM алгоритм создает реальный DOM из VDOM (наше приложение).
Не волнуйтесь, сейчас мы всё разберём!
Перед тем, как попасть в волшебную страну под названием VDOM, давайте узнаем побольше о JSX.
1. Babel и JSX
JSX позволяет нам писать HTML в JavaScript! А также даёт возможность использовать в нём JS (правда в фигурных скобках <>).
Виртуальный DOM и детали его реализации в React¶
Что такое виртуальный DOM?¶
Виртуальный DOM (VDOM) — это концепция программирования, в которой идеальное или «виртуальное» представление пользовательского интерфейса хранится в памяти и синхронизируется с «настоящим» DOM при помощи библиотеки, такой как ReactDOM. Этот процесс называется согласованием.
Такой подход и делает API React декларативным: вы указываете, в каком состоянии должен находиться пользовательский интерфейс, а React добивается, чтобы DOM соответствовал этому состоянию. Это абстрагирует манипуляции с атрибутами, обработку событий и ручное обновление DOM, которые в противном случае пришлось бы использовать при разработке приложения.
Поскольку «виртуальный DOM» – это скорее паттерн, чем конкретная технология, этим термином иногда обозначают разные понятия. В мире React «виртуальный DOM» обычно ассоциируется с React-элементами, поскольку они являются объектами, представляющими пользовательский интерфейс. Тем не менее, React также использует внутренние объекты, называемые «волокнами» (fibers), чтобы хранить дополнительную информацию о дереве компонентов. Их также можно считать частью реализации «виртуального DOM» в React.
Теневой DOM похож на виртуальный DOM?¶
Нет, они совсем разные. Теневой DOM (Shadow DOM) — это браузерная технология, предназначенная в основном для определения переменных и CSS в веб-компонентах. Виртуальный DOM – это концепция, реализованная библиотеками в JavaScript поверх API браузера.
Что такое «React Fiber»?¶
Fiber – новый механизм согласования в React 16, основная цель которого сделать рендеринг виртуального DOM инкрементным. Узнать больше об этом.
Полное руководство по виртуальной DOM React
Дата публикации: 2021-06-16
От автора: борьба с бесполезными манипуляциями с DOM, согласование и алгоритм различий.
Реальная DOM
Перво-наперво, DOM означает «объектную модель документа». Простыми словами DOM представляет пользовательский интерфейс вашего приложения. Каждый раз, когда происходит изменение состояния пользовательского интерфейса вашего приложения, DOM обновляется, чтобы представить это изменение. Теперь загвоздка заключается в том, что частые манипуляции с DOM влияют на производительность, делая ее медленной.
Что замедляет манипуляции с DOM?
Модель DOM представлена в виде древовидной структуры данных. Из-за этого изменения и обновления в DOM происходят быстро. Но после изменения обновленный элемент и его дочерние элементы должны быть повторно отрисованы, чтобы обновить пользовательский интерфейс приложения. Повторный рендеринг или перерисовка пользовательского интерфейса — вот что делает его медленным. Следовательно, чем больше у вас компонентов пользовательского интерфейса, тем дороже могут быть обновления DOM, поскольку их нужно будет повторно отображать при каждом обновлении DOM.
Манипуляции с DOM — это сердце современного интерактивного Интернета. К сожалению, это также намного медленнее, чем большинство операций JavaScript. Эта медлительность усугубляется тем фактом, что большинство фреймворков JavaScript обновляют DOM гораздо чаще, чем необходимо.
В качестве примера предположим, что у вас есть список из десяти элементов. Вы отмечаете первый пункт. Большинство фреймворков JavaScript перестраивают весь список. Это в десять раз больше работы, чем необходимо! Изменился только один элемент, а остальные девять будут восстановлены точно такими же, как и раньше.
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Перестроение списка не представляет большого труда для веб-браузера, но современные веб-сайты могут использовать огромное количество манипуляций с DOM. Неэффективное обновление стало серьезной проблемой. Чтобы решить эту проблему, люди которые работают с React популяризировали нечто, называемое виртуальным DOM.
Виртуальная DOM
В React для каждого объекта DOM существует соответствующий «виртуальный объект DOM». Виртуальный объект DOM — это представление объекта DOM, подобно его облегченной копии. Виртуальный объект DOM имеет те же свойства, что и реальный объект DOM, но у него нет реальной возможности напрямую изменять то, что отображается на экране.
«Виртуальная модель DOM (VDOM) — это концепция программирования, в которой идеальное или «виртуальное» представление пользовательского интерфейса хранится в памяти и синхронизируется с« реальной » библиотекой DOM, такой как ReactDOM. Этот процесс называется согласованием».
Манипулирование DOM происходит медленно. Управление виртуальным DOM происходит намного быстрее, потому что на экране ничего не отображается. Думайте о манипулировании виртуальной DOM как о редактировании чертежа, а не о перемещении комнат в реальном доме.
Как виртуальный DOM работает быстрее?
Когда в пользовательский интерфейс добавляются новые элементы, создается виртуальная модель DOM, представленная в виде дерева. Каждый элемент является узлом в этом дереве. Если состояние любого из этих элементов изменяется, создается новое виртуальное дерево DOM. Затем это дерево сравнивается с предыдущим виртуальным деревом DOM.
Как только это будет сделано, виртуальная DOM вычисляет наилучший из возможных методов внесения этих изменений в реальную DOM. Это гарантирует минимальное количество операций с реальной DOM. Следовательно, снижение стоимости обновления реальной модели DOM.
На изображении ниже показано виртуальное дерево DOM и процесс сравнения.
Красные кружки представляют собой узлы, которые изменились. Эти узлы представляют элементы пользовательского интерфейса, состояние которых было изменено. Затем вычисляется разница между предыдущей версией виртуального дерева DOM и текущим виртуальным деревом DOM. Затем все родительское поддерево повторно визуализируется для получения обновленного пользовательского интерфейса. Это обновленное дерево затем пакетно обновляется до реальной модели DOM.
Как React использует виртуальную модель DOM?
Теперь, когда у вас есть четкое представление о том, что такое виртуальная модель DOM и как она может повысить производительность вашего приложения, давайте посмотрим, как React использует виртуальную модель DOM.
1. React следует шаблону наблюдателю и отслеживает изменения состояния.
В React каждая часть пользовательского интерфейса является компонентом, и каждый компонент имеет состояние. Когда состояние компонента изменяется, React обновляет виртуальное дерево DOM. После обновления виртуальной DOM, React сравнивает текущую версию виртуальной DOM с предыдущей версией виртуальной DOM. Этот процесс называется «дифференцированием».
Как только React узнает, какие виртуальные объекты DOM были изменены, он обновляет только эти объекты в реальной DOM. Это значительно повышает производительность по сравнению с непосредственным манипулированием реальной DOM. Это выделяет React как высокопроизводительную библиотеку JavaScript.
2. React следует механизму пакетного обновления для обновления реальной DOM.
Следовательно — приводит к увеличению производительности. Это означает, что обновления реальной модели DOM отправляются пакетами вместо отправки обновлений для каждого отдельного изменения состояния.
Перекраска пользовательского интерфейса — самая дорогостоящая часть, и React гарантирует, что реальный DOM получает только пакетные обновления для перерисовки пользовательского интерфейса.
3. React следует эффективному алгоритму различий.
React реализует эвристический алгоритм O (n), основанный на двух предположениях:
Два элемента разных типов будут давать разные деревья.
Разработчик может установить, какие дочерние элементы могут быть стабильными при разных рендерингах с помощью свойства key.
На практике эти предположения верны почти для всех случаев использования. При рассмотрении двух деревьев React сначала сравнивает два корневых элемента. Поведение различается в зависимости от типов корневых элементов.
Элементы разных типов
Всякий раз, когда корневые элементы имеют разные типы, React удаляет старое дерево и строит новое дерево с нуля.
При удалении дерева старые узлы DOM уничтожаются. Экземпляры компонентов получают состояние componentWillUnmount(). При построении нового дерева новые узлы DOM вставляются в DOM. Экземпляры компонентов получают состояние UNSAFE_componentWillMount() а затем componentDidMount(). Любое состояние, связанное со старым деревом, теряется.
Любые компоненты ниже корневого также будут размонтированы, и их состояние будет уничтожено. Пример, для сравнения:
Это уничтожит старый Counter и перемонтирует новый.
Элементы одного типа
При сравнении двух элементов React DOM одного типа React смотрит на атрибуты обоих, сохраняет один и тот же базовый узел DOM и обновляет только измененные атрибуты. Например:
Сравнивая эти два элемента, React знает, что нужно изменять только базовый узел className. При обновлении style, React также знает, что нужно обновлять только те свойства, которые изменились. Например:
При преобразовании между этими двумя элементами React знает, что нужно изменять только стиль color, а не fontWeight. После обработки узла DOM React рекурсивно обращается к дочерним элементам.
Рекурсия по дочерним элементам
По умолчанию при рекурсии по дочерним элементам узла DOM, React просто выполняет итерацию по обоим спискам дочерних элементов одновременно и генерирует мутацию всякий раз, когда есть разница. Например, при добавлении элемента в конце дочерних элементов преобразование между этими двумя деревьями работает хорошо:
Если вы реализуете это без Virtual DOM, вставка элемента в начале имеет худшую производительность. Например, преобразование между этими двумя деревьями работает плохо:
Использование ключей
Чтобы решить эту проблему, React поддерживает атрибут key. Когда у потомков есть ключи, React использует ключ для сопоставления их в исходном дереве с потомками в последующем дереве. Например, добавление key к нашему неэффективному примеру выше может сделать преобразование дерева эффективным:
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Теперь React знает, что элемент с ключом ’2014′ является новым, а элементы с ключами ’2015′ и ’2016′ только переместились.
На практике найти ключ обычно не сложно. Элемент, который вы собираетесь отображать, может уже иметь уникальный идентификатор, поэтому ключ может быть просто получен из ваших данных:
Если это не так, вы можете добавить новое свойство ID в свою модель или хешировать некоторые части контента для генерации ключа. Ключ должен быть уникальным только среди своих братьев и сестер, а не глобально.
В крайнем случае вы можете передать индекс элемента в массиве в качестве ключа. Это может сработать, если элементы никогда не переупорядочиваются, но переупорядочивание будет медленным.
Изменение порядка также может вызвать проблемы с состоянием компонента, когда индексы используются в качестве ключей. Экземпляры компонентов обновляются и повторно используются в зависимости от их ключа. Если ключ является индексом, перемещение элемента изменяет его. В результате состояние компонентов для таких вещей, как неконтролируемые вводы, может смешиваться и обновляться неожиданным образом.
Проще говоря: «Вы сообщаете React, в каком состоянии вы хотите, чтобы находился пользовательский интерфейс, и он гарантирует, что DOM соответствует этому состоянию. Большим преимуществом здесь является то, что вам как разработчику не нужно знать, как делать манипуляции с атрибутами, обработку событий или ручное обновление DOM за кулисами».
Все эти детали абстрагируются от разработчиков React. Все, что вам нужно сделать, это обновить состояния вашего компонента по мере необходимости, а React позаботится обо всем остальном. Это обеспечивает превосходную производительность разработчика при использовании React.
Поскольку «виртуальный DOM» — это скорее шаблон, чем конкретная технология, люди иногда говорят, что это означает разные вещи. В мире React термин «виртуальный DOM» обычно ассоциируется с элементами React, поскольку они являются объектами, представляющими пользовательский интерфейс. Однако React также использует внутренние объекты, называемые «волокнами», для хранения дополнительной информации о дереве компонентов. Их также можно рассматривать как часть реализации «виртуальной DOM» в React. Fiber — это новый механизм согласования в React 16. Его основная цель — включить инкрементный рендеринг виртуальной DOM.
Как выглядит виртуальная модель DOM?
Название «виртуальный DOM», как правило, добавляет загадочности тому, что это за концепция на самом деле. Фактически, виртуальная модель DOM — это просто обычный объект Javascript. Вернемся к дереву DOM, которое мы создали ранее:
Это дерево также может быть представлено как объект Javascript.
Как написать свой Virtual DOM
Всем привет! Сегодня нас ждет удивительное приключение реализации виртуального DOM-a с нуля.
Материал получился большим, чтение у вас может занять до получаса, имейте это ввиду. А если у вас нет столько времени, то вы можете сразу посмотреть результат в песочнице codesandbox или на гитхабе.
Быстрые переходы по частям:
Что такое Virtual DOM? #
Если речь заходит про виртуальный DOM, то почти всегда дело касается React-а. Но на самом деле это общая концепция, используемая и за пределами мира React-а.
Например, у нас есть следующий код:
Виртуальное представление может выглядеть следующим образом:
Такой паттерн позволяет описывать интерфейсы в декларативном подходе, абстрагирует нас от прямой работы с DOM, обработчиками событий и атрибутов, и самое важное: оптимизирует работу обновления только нужных частей DOM-дерева (отвечает за синхронизацию с реальным DOM).
На медиуме есть классный пост, как работает VDOM в React-е, а мы реализуем его сами (конечно очень упрощенную, но рабочую версию). Приступим!
createVNode(tagName, props, children) #
Реализация этого метода довольна простая, поэтому приступим:
Метод принимает три параметра:
Для свойств и детей используем значения по умолчанию, чтобы при вызове createVNode(‘h1’) быть уверенным, что у созданной виртуальной ноды будут проставлены как свойства, так и дети, и избегать этих проверок в дальнейшем.
Напишем наш первый виртуальный элемент:
В результате мы получим следующий объект:
Придумаем что-нибудь посложнее:
И посмотрим на вывод в терминале:
Мы только что научились строить виртуальное дерево или виртуальный DOM, поздравляю! Но это только самое начало. Далее мы будет из этого дерева получать реальные DOM элементы.
createNode(vNode) #
Теперь из виртуального дерева необходимо сформировать DOM узел. Рассмотрим реализацию ниже:
Вы можете заметить, что рекурсия не лучшее решение, особенно для обхода DOM-узлов, вложенность которых может быть очень глубокой, и лучше использовать стек (чтобы не словить ошибку при достижении лимитов рекурсии). И вы будете правы, но для простоты реализации оставим именно этот вариант.
Попробуем метод в действии:
После выполнения кода в терминале будет отображено полученное DOM-дерево:
Очередной шаг завершен. Мы научились строить виртуальное DOM-дерево и формировать из него реальное. Теперь нужно отобразить результат в DOM страницы.
mount(node, target) #
Нам понадобится index.html страница, на которую и будем монтировать полученное DOM-дерево:
Используем метод mount в нашем приложении и посмотрим на результат в браузере:
Состояние #
Усложним пример и добавим на страницу состояние. Конечно же, что может быть лучше старого доброго счетчика, который мы и добавим. Будем обновлять каждую секунду счетчик (с помощью setInterval ) и перерисовывать DOM-дерево.
И добавим код с интервалом и перерендером:
Вернувшись в браузер, мы увидим, что счетчик обновляется, как мы и планировали! На данном этапе мы можем писать приложения в декларативном стиле, что интуитивно понятно и просто.
Но есть и несколько серьезных проблем:
Чтобы увидеть реальные перерисовки браузера, можно включить в devtools браузера вкладку Rendering и поставить галку у пункта «Paint flashing» (области, которые браузер перерисовывает, будут подсвечиваться зеленым цветом):
И посмотрим на результат:
Браузер перерисовывает все элементы, несмотря на то, что часть из них не меняется.
patchNode(node, vNode, nextVNode) #
Выглядеть это будет следующим образом:
patchProps(node, props, nextProps) #
Вполне логичным замечанием будет то, почему мы мержим старые и новые свойства и перебираем их, когда старые достаточно было бы удалить. В разделе с обработчиками событий нам это пригодится.
Обновление самого свойства вынесем в дополнительный метод patchProp :
В нашей реализации мы этого делать не будем.
patchChildren(. ) #
patchChildren(parentNode, vChildren, nextVChildren)
Метод выглядит следующим образом:
С помощью slice мы получаем массив из дочерних нод, которые необходимо просто вставить в родительскую DOM-ноду.
Так же можно для виртуальных нод добавить поле key, чтобы использовать его в списках, как это сделано в React, и при добавлении элемента в начало списка не обновлять все следующие ноды. Но этот пункт мы так же пропустим.
Собираем все вместе #
Файл vdom.js содержит 120 строк кода и реализует простейший вариант Virtual DOM-a. Попробуем его в действии: будем на каждое обновление стейта формировать новый виртуальный DOM и патчить ноду.
Посмотрим на результат в браузере:
А так же в DevTools на изменения в DOM-дереве:
patch(vNode, node) #
Используем этот метод в patch :
И перепишем app.js (теперь можно не использовать метод mount и обойтись patch ):
Конечно же вручную запускать patch на каждое обновление стейта не хочется, поэтому усложним несколько наше состояние, добавив метод setState и слушателя onStateChanged :
Создавали VDOM, а по ходу реализовали очень легкую версию flux-стора. Теперь достаточно только обновлять state и DOM автоматически будет обновлен:
Финальный вариант файла app.js будет выглядеть следующим образом:
Обработчики событий #
И добавим две кнопки в createVApp :
Запустив этот код, кнопки появятся, но при нажатии на них ничего не будет происходить. Почему так? Если посмотреть в исходный код, то увидим, что код обработчиков (функции) находятся в атрибуте onclick в виде строки. И даже если бы он вызывался, то произошла бы ошибка, так как переменная store не определена.
Чтобы сохранить контекст вызова, нужно сохранить обработчик прямо в DOM-ноду. Сперва создадим функцию listerner :
Зачем она нам нужна и что делает? Эта функция будет вызываться при вызове события (например click ), this указывает на DOM-элемент, this[event.type] на метод, который мы указываем в виртуальном элементе.
Проверяем работу событий в действии:
Интегрируем JSX #
будет скомпилирован в следующий JS-код:
Нужно для приведения возвращаемого значения к формату hyperscript.
Теперь изменим наш пример:
Итоги #
Сегодня мы проделали большую работу, реализовав свою версию Virtual DOM с блекджеком, обработчиками событий, методом патчинга реальной ноды и даже интегрировали JSX.
Итоговый вариант залит на гитхаб и в codesandbox (версия без jsx).
В процессе написания поста я наткнулся на библиотеку superfine, которая предоставляет свою реализацию Virtual DOM в 300 строк кода. Если хотите погрузиться в тему поглубже, то рекомендую начать с изучения именно данного решения (в ней есть работа с key в списках, обработка svg, выполнение на стороне сервера и многое другое).
Что такое Virtual DOM?
За последний год я много слышал о Virtual DOM и React JS.
React работает действительно быстро и очень прост, но как он работает? Что такое Virtual DOM? Почему я должен беспокоиться об этом, и что случилось со старым добрым обычным DOM?
Что такое DOM
Перед тем, как мы начнем вникать в то, что из себя представляет DOM виртуальный, давайте немного поговорим о том, чем является DOM реальный.
DOM (аббревиатура от Document Object Model) — способ представления структурного документа с помощью объектов. Это кроссплатформенное и языко-независимое соглашение для представления и взаимодействия с данными в HTML, XML и т.д.
Веб-браузеры обрабатывают составляющие DOM, и мы можем взаимодействовать с ними, используя JavaScript и CSS. Мы можем работать с узлами документа, изменять их данные, удалять и вставлять новые узлы. В наши дни DOM API является практически кроссплатформенным и кроссбраузерным.
Так в чем же проблема?
Проблема DOM
Главная проблема DOM — он никогда не был рассчитан для создания динамического пользовательского интерфейса (UI). Мы можем работать с ним, используя JavaScript и библиотеки наподобие jQuery, но их использование не решает проблем с производительностью.
Посмотрите на современные социальные сети, такие как Twitter, Facebook или Pinterest.
После небольшого скроллинга, мы будем иметь десятки тысяч DOM-узлов, эффективно взаимодействовать с которыми — задача не из легких.
Для примера, попробуйте переместить 1000 div-блоков на 5 пикселей влево.
Это может занять больше секунды — это слишком много для современного интернета. Вы можете оптимизировать скрипт и использовать некоторые приемы, но в итоге это вызовет лишь головную боль при работе с огромными страницами и динамическим UI.
Можем ли мы решить эту проблему? Похоже, что можем.
В настоящее время W3C работает над новым стандартом Shadow DOM.
Shadow DOM — это рабочий черновик стандарта W3C. Спецификация, описывающая метод объединения нескольких DOM-деревьев в одну иерархию и как эти деревья взаимодействуют друг с другом в пределах документа, что позволяет лучше скомпоновать DOM.
Другой вариант заключается в использовании подхода с Virtual DOM.
Virtual DOM не является стандартом и в конечном итоге мы по-прежнему взаимодействуем с DOM, но делаем это как можно реже и более эффективно.
Virtual DOM
Вместо того, чтобы взаимодействовать с DOM напрямую, мы работаем с его легковесной копией. Мы можем вносить изменения в копию, исходя из наших потребностей, а после этого применять изменения к реальному DOM.
При этом происходит сравнение DOM-дерева с его виртуальной копией, определяется разница и запускается перерисовка того, что было изменено.
Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM.
Но только если мы делаем это правильно. Есть две проблемы: когда именно делать повторную перерисовку DOM и как это сделать эффективно.
React JS
React JS — это JavaScript-библиотека, разработанная в Facebook для создания пользовательских интерфейсов, которая популяризировала идею использования виртуального DOM. React создает легковесное дерево из JavaScript-объектов для имитации DOM-дерева. Затем он создает из них HTML, который вставляется или добавляется к нужному DOM-элементу, что вызывает перерисовку страницы в браузере.
React — это библиотека, а не фреймворк, поэтому сравнивать его с Angular или Ember некорректно.
Другие библиотеки и фреймворки
Вывод
Virtual DOM — это техника и набор библиотек / алгоритмов, которые позволяют нам улучшить производительность на клиентской стороне, избегая прямой работы с DOM путем работы с легким JavaScript-объектом, имитирующем DOM-дерево.
Идея с использованием виртуального DOM отличная, хотя и не нова — мы давно знали, что прямая работа с DOM обходится дорого. Используя библиотеки наподобие React, мы можем повысить производительность приложений и сделать это очень просто.