React Virtual DOM Это не ракетная наука

Если вы работаете с react или планируете стать разработчиком react, вы должны знать, что виртуальный DOM react будет неизбежным вопросом на собеседовании с разработчиками react. Знаете, подготовка к собеседованию может разочаровать, так много всего нужно изучить, понять, и, возможно, как я, вам приходится заново изучать концепции, которые, как предполагается, вы уже знаете (потому что вы претендуете на руководящую должность 😵💫), но, честно говоря, я обнаружил, что изучаю эту концепцию снова и снова, и это просто означает, что я действительно не понимаю, как она работает! 🤦🏻♀️

Итак, в этом посте я постараюсь сделать все возможное, чтобы объяснить, как работает виртуальный DOM, возможно, это не может быть полезным для кого-то еще, кроме меня (надеюсь, это может помочь кому-то еще 🤞🏻), но я читал, что иногда вы сохраняете больше информации, если записываете ее, так что это мой эксперимент, чтобы проверить, действительно ли это работает. Итак, без лишних слов, давайте начнем.

Прежде чем начать разговор о виртуальной DOM, давайте сделаем краткое резюме о том, как работает браузерная DOM (Document Object Model).

Что такое браузерная DOM?

Когда вы делаете запрос на сервер для получения содержимого страницы, сервер возвращает файл в формате двоичного потока (в основном единицы и нули) с определенным типом содержимого, например Content-Type: Text/html; charset=UTF-8 это говорит браузеру, что это HTML документ (также может быть XML документ) и закодирован в UTF-8. Получив эту информацию, браузер может прочитать HTML-код. Первоначально для каждого HTML-тега браузер создает узел, например, тег <div> элемент создается из HTMLDivElement, который наследуется от класса Node. Когда все элементы созданы, браузер создает древовидную структуру с этими объектами-узлами. И выглядеть она будет следующим образом:

DOM также является API, где вы можете получить доступ к этим узлам для чтения и изменения, и это делается через объект document, используя, например, document.querySelector("p") (Подробнее читайте здесь).

Что такое виртуальный DOM?

Виртуальный DOM (VDOM) — это концепция программирования, в которой «виртуальное» представление пользовательского интерфейса (UI) хранится в памяти (памяти браузера) и синхронизируется с «реальным» DOM (DOM браузера 👆🏻), и это делается с помощью такой библиотеки, как ReactDOM. Этот процесс называется согласованием (Reconciliation).

Другими словами, React делает копию «реального» DOM и сравнивает узлы между виртуальным и реальным DOM, чтобы увидеть, какие узлы изменились, какие были добавлены, удалены или обновлены. Как только различия выявлены, React просто обновляет узлы, которые отличаются, и это является ключом к их высокой производительности.

Давайте соберем все вместе 🧐 — Когда мы загружаем сайт в первый раз, наш браузер по умолчанию создает в памяти структуру данных (она же DOM), которая представляет собой дерево узлов, где каждый узел представляет собой HTML-тег вместе с его свойствами. React должен создать виртуальное представление этого дерева, но это должно быть сделано эффективным способом. Итак, как реакт это делает? Ну, поскольку DOM может быть очень большим и сложным для манипуляций, React создает уменьшенную копию, хранящую только ту часть DOM, которую он действительно будет использовать, и обычно это div root.

ReactDOM.render(element, document.getElementById('root'))

В начале наш браузер имеет пустую структуру только с корневым узлом <div id="root">, react создает виртуальный DOM со всей структурой, которую мы добавляем в наш основной компонент, например <App/>, и когда выполняется метод ReactDOM.render(), все узлы, существующие в виртуальном DOM, выталкиваются в реальный DOM.

В первый раз ReactDOM.render() рендерит все приложение, но после этого первого рендеринга react будет определять изменения на различных узлах, сравнивать предыдущее состояние с новым и применять рендеринг только для этих узлов, которые изменились.

Примечание: render был заменен на createRoot в React 18 читать далее

Что происходит во время рендеринга?

Важно понимать, что происходит во время рендеринга. Для этого нам нужно знать, как React работает с родными HTML-тегами и с компонентами, которые мы создали.

Поэтому давайте рассмотрим это на примере:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
console.log('___<App/>', <App/>)
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Вход в полноэкранный режим Выход из полноэкранного режима

У нас есть довольно простое приложение, мы печатаем console.log с <App/> в качестве значения, и вот что оно возвращает

Как вы видите, свойство type для <App/> является функцией, поэтому давайте посмотрим поведение для собственного HTML-тега.

console.log(<div id="test">I'm a div</div>)

Добавив этот console.log родного HTML-тега, мы получим:

здесь type является "div" имеет некоторые реквизиты children и id.

Итак, почему это важно? Потому что при рендере библиотека ReactDOM должна «транспонировать» все эти узлы в правильный JSX-код, чтобы они были действительны для DOM. Таким образом, для компонентов у нас есть функция типа, и эта функция должна быть выполнена reactDOM, чтобы получить эквивалентный узел, действительный для DOM.

console.log(App())

Добавив console.log приложения не как компонент, а как функцию, мы получим:

теперь у нас есть допустимый тип "div" с соответствующими props и children, так что это узел, допустимый для добавления в DOM.

Что насчет процесса согласования?

Процесс согласования — это сердце ❤️ того, как React действительно обновляет только те узлы, которые изменились, поэтому давайте посмотрим, как он работает.

React предоставляет декларативный API, поэтому вам не нужно беспокоиться о том, что именно меняется при каждом обновлении. Это значительно упрощает написание приложений, но может быть неочевидно, как это реализовано в React.

React должен реализовать алгоритм, чтобы выяснить, как эффективно обновить пользовательский интерфейс в соответствии с последним деревом. Существует несколько типовых решений для решения этой алгоритмической проблемы, заключающейся в генерации минимального количества операций для преобразования одного дерева в другое. Однако все типовые решения имеют сложность O(n^3), где n — количество элементов в дереве (если вы не знакомы с нотацией Big O, я рекомендую посмотреть это видео).

Если мы реализуем это на React, отображение 1000 элементов потребует порядка миллиарда сравнений. Это слишком дорого. Вместо этого React реализует эвристический алгоритм O(n), основанный на двух предположениях:

  1. Два элемента разных типов будут создавать разные деревья.
  2. Разработчик может намекнуть, какие дочерние элементы могут быть стабильными при различных рендерах с помощью параметра key.

Элементы разных типов

Если корневые элементы имеют разные типы, React разрушит старое дерево и построит новое с нуля. Переход от <a> к <img> , или от <Article> к <Comment> , или от <Button> к 

любой из них приведет к полной перестройке.

<div>
  <Counter/>
</div>

<span>
  <Counter/>
</span>

При этом старый Counter будет уничтожен, а новый установлен заново.

Элементы DOM одного типа

При сравнении двух элементов React DOM одного типа, React просматривает атрибуты обоих, сохраняет один и тот же базовый узел DOM и обновляет только измененные атрибуты. Например:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

Сравнивая эти два элемента, React знает, что нужно изменить только className на базовом узле DOM.

Обращение к дочерним элементам

По умолчанию, при рекурсии по дочерним элементам узла DOM, React просто итерирует оба списка дочерних элементов одновременно и генерирует мутацию, когда есть разница.

Например, при добавлении элемента в конец дочерних узлов, преобразование между этими двумя деревьями работает хорошо:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React сопоставит два дерева <li>first</li>, сопоставит два дерева <li>second</li>, а затем вставит дерево <li>third</li>.

Если реализовать это наивно, вставка элемента в начале имеет худшую производительность. Например, преобразование между этими двумя деревьями работает плохо:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React будет мутировать каждого ребенка, вместо того чтобы понять, что он может сохранить поддеревья <li>Duke</li> и <li>Villanova</li> нетронутыми. Эта неэффективность может стать проблемой.

Важность ключей 😱.

Чтобы решить эту проблему, React поддерживает атрибут key. Когда у детей есть ключи, React использует ключ для сопоставления детей в исходном дереве с детьми в последующем дереве. Например, добавление key к нашему неэффективному примеру выше может сделать преобразование дерева эффективным:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

Теперь React знает, что элемент с ключом '2014' является новым, а элементы с ключами '2015' и '2016' только что переместились.

Подробнее о процессе согласования вы можете прочитать здесь

React Fiber?

Fiber — это новый механизм согласования в React 16. Его основная цель — обеспечить инкрементный рендеринг виртуального DOM. Это сложная концепция, по сути, этот новый алгоритм является переделкой старых версий согласователя React, имеет некоторые улучшения в определении приоритетов порядка рендеринга, разрывает границы стека вызовов и позволяет приостанавливать или начинать работу по рендерингу там, где это необходимо. Подробнее вы можете прочитать здесь и здесь


Ок, я думаю, что это конец, пожалуйста, дайте мне комментарий, если я в чем-то не прав, или если вы чувствуете, что есть что-то, что должно быть добавлено, или просто если это было полезно для вас 😊. 

Спасибо большое, что нашли время прочитать это!!! 🙇🏻♀️

Оцените статью
Procodings.ru
Добавить комментарий