Виртуальный DOM в React

8 минут читать
Разработка
react javascript
Виртуальный DOM в React

Введение

Привет, это Глеб Котовский!
Сегодня я хочу поговорить о Виртуальном DOM, а именно — о реализации в React.

Виртуальный DOM (Virtual Document Object Model) — это крутая концепция в программировании, которая хранит “виртуальное” представление пользовательского интерфейса в памяти. Это представление синхронизируется с реальным DOM браузера (Document Object Model) через библиотеку.
Вы обнаружите, что виртуальный DOM играет важную роль во многих JavaScript-фреймворках для фронтенда, и это одна из причин их высокой эффективности. В этой статье мы погрузимся в то, как работает виртуальный DOM в React и почему он важен для этой библиотеки.

Что такое DOM?

Когда веб-страница загружается в браузере, она обычно получает HTML-документ с сервера. Браузер затем строит из этого HTML логическую древовидную структуру для отображения запрошенной страницы пользователю. Эта структура известна как DOM.
Document Object Model (DOM) представляет собой логическое дерево, описывающее документ. Каждая ветвь дерева заканчивается узлом (node), который содержит объект. Поскольку браузер преобразует документ в эту древовидную структуру, необходимы методы для программного доступа к дереву, позволяющие изменять структуру, стиль или содержимое документа. Эта потребность привела к созданию DOM API, который предоставляет методы для манипуляции узлами, представляющими элементы в дереве.

Объекты DOM

Реализация Виртуального DOM в React

Чтобы оптимизировать перерисовку в веб-сайтах и приложениях, многие JavaScript-фреймворки предлагают различные стратегии. Однако React использует концепцию виртуального DOM.
Виртуальный DOM в React представляет пользовательский интерфейс как “виртуальное” дерево, где каждый элемент — это узел-объект. Это представление хранится в памяти и синхронизируется с реальным DOM браузера через библиотеку React DOM.
В то время как React и многие другие известные фреймворки используют Виртуальный DOM, Svelte работает напрямую с DOM в браузере и изменяет его по мере необходимости.

Вот простой пример, иллюстрирующий Виртуальный DOM в React-компоненте:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

В этом примере:

  • Компонент отображает счетчик и кнопку.
  • При клике на кнопку обновляется состояние, что заставляет React создать новое дерево Виртуального DOM.
  • Алгоритм сравнения (diffing) проверяет, что изменилось (только счетчик), и обновляет реальный DOM соответственно.

После первого рендера компонента при состоянии count: 0, реальный DOM будет выглядеть так:

<div>
  <h1>Counter</h1>
  <p>Count: 0</p>
  <button>Increment</button>
</div>

Как работает Виртуальный DOM:

Вот пошаговая иллюстрация работы Виртуального DOM на примере компонента:

1. Определение компонента

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return (
    <div>
      <h1>Counter</h1>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

2. Процесс первоначального рендера

2.1 Инициализация компонента

При первом рендере React вызывает функцию Counter.

2.2 Инициализация состояния

useState(0) устанавливает начальное состояние компонента в 0.

2.3 Создание Виртуального DOM

React генерирует дерево Виртуального DOM на основе возвращаемой JSX-структуры. Это дерево — легковесное представление UI.
Для первоначального рендера Виртуальный DOM выглядит так:

{
  "type": "div",
  "props": {
    "children": [
      { "type": "h1", "props": { "children": "Counter" } },
      { "type": "p", "props": { "children": "Count: 0" } },
      { "type": "button", "props": { "children": "Increment" } }
    ]
  }
}

2.4 Обновление реального DOM

React берет этот Виртуальный DOM и вычисляет, какие изменения нужно внести в реальный DOM. В данном случае создается такой HTML:

<div>
  <h1>Counter</h1>
  <p>Count: 0</p>
  <button>Increment</button>
</div>

3. Взаимодействие пользователя

При клике на кнопку “Increment” происходят следующие шаги:

3.1 Обработка события

Событие onClick кнопки вызывает функцию increment, которая выполняет setCount(count + 1).

3.2 Обновление состояния

Состояние компонента обновляется, что заставляет React понять: компонент нужно перерисовать с новым состоянием.

4. Процесс перерисовки

4.1 Повторный вызов компонента

React снова вызывает функцию Counter из-за изменения состояния.

4.2 Создание нового Виртуального DOM

Создается новое дерево Виртуального DOM с обновленным состоянием:

{
  "type": "div",
  "props": {
    "children": [
      { "type": "h1", "props": { "children": "Counter" } },
      { "type": "p", "props": { "children": "Count: 1" } },
      { "type": "button", "props": { "children": "Increment" } }
    ]
  }
}

4.3 Сравнение Виртуальных DOM

React сравнивает новый Виртуальный DOM с предыдущим. Он определяет изменения — в данном случае изменился текст в теге <p> с “Count: 0” на “Count: 1”.

4.4 Согласование (Reconciliation)

Обновляются только измененные части реального DOM. В этом случае React обновляет реальный DOM для отражения нового счетчика:

<div>
  <h1>Counter</h1>
  <p>Count: 1</p> <!-- Обновленное содержимое -->
  <button>Increment</button>
</div>

5. Оптимизация производительности

5.1 Пакетное обновление

Если происходит несколько обновлений состояния подряд (например, несколько кликов по кнопке), React может сгруппировать их для эффективности, минимизируя количество перерисовок и обновлений DOM.

Распространенные проблемы с Виртуальным DOM в React и как их избежать

  1. Узкие места производительности
    • Проблема: Чрезмерные перерисовки могут происходить даже с Виртуальным DOM.
    • Решение: Используйте React.memo для мемоизации функциональных компонентов.
const MyComponent = React.memo(({ value }) => {
  console.log('Rendered: ', value);
  return <div>{value}</div>;
});

Для классовых компонентов: Используйте shouldComponentUpdate:

class MyClassComponent extends React.Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.value !== this.props.value;
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}
  1. Неэффективное управление ключами
    • Проблема: Неправильная обработка ключей в списках может вызывать ошибки.
    • Решение: Используйте уникальные и стабильные ключи, а не индексы массива.
const items = ['Apple', 'Banana', 'Cherry'];
return (
  <ul>
    {items.map(item => (
      <li key={item}>{item}</li> // Используйте уникальные значения как ключи
    ))}
  </ul>
);
  1. Чрезмерное использование состояния и обновлений
    • Проблема: Слишком много обновлений состояния ведет к проблемам производительности.
    • Решение: Объединяйте связанные состояния
const [state, setState] = useState({
  name: '',
  age: 0,
});

const updateAge = (newAge) => {
  setState(prevState => ({ ...prevState, age: newAge }));
};
  1. Использование inline-функций
    • Проблема: Inline-функции создают новые экземпляры при каждом рендере.
    • Решение: Используйте useCallback для мемоизации функций.
const increment = useCallback(() => {
  setCount(c => c + 1);
}, []); // Пересоздавать функцию только при изменении зависимостей
  1. Глубокие деревья компонентов
    • Проблема: Глубоко вложенные компоненты вызывают множественные перерисовки.
    • Решение: Используйте контекст.
const CountContext = React.createContext();

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={{ count, setCount }}>
      <ChildComponent />
    </CountContext.Provider>
  );
};

const ChildComponent = () => {
  const { count, setCount } = useContext(CountContext);
  return <div onClick={() => setCount(count + 1)}>Count: {count}</div>;
};
  1. Избыточные перерисовки из-за обновлений родителя
    • Проблема: Дочерние компоненты перерисовываются при обновлении родителя.
    • Решение: Мемоизируйте дочерние компоненты.
const ChildComponent = React.memo(({ count }) => {
  return <div>Count: {count}</div>;
});
  1. Неэффективный рендер ресурсоемких компонентов
    • Проблема: Ресурсоемкие компоненты могут замедлять приложение.
    • Решение: Используйте React.lazy и React.Suspense.
const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => (
  <React.Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </React.Suspense>
);
  1. Управление побочными эффектами
    • Проблема: Побочные эффекты могут вызывать ошибки при неправильном управлении.
    • Решение: Используйте useEffect с правильными зависимостями.
useEffect(() => {
  const timer = setTimeout(() => {
    console.log('Время прошло');
  }, 1000);

  return () => clearTimeout(timer); // Очистка при размонтировании или изменении зависимостей
}, [зависимости]); // Замените реальными зависимостями
  1. Путаница между состоянием и пропсами
    • Проблема: Непонимание, когда использовать state vs props.
    • Решение: Используйте props для внешних данных и state для локальных данных.
const ParentComponent = () => {
  const [name, setName] = useState('John');

  return <ChildComponent name={name} setName={setName} />;
};

const ChildComponent = ({ name, setName }) => (
  <div>
    <p>{name}</p>
    <button onClick={() => setName('Jane')}>Change Name</button>
  </div>
);
  1. Пренебрежение доступностью
    • Проблема: Игнорирование вопросов доступности.
    • Решение: Используйте семантический HTML и инструменты доступности.
const AccessibleButton = () => (
  <button aria-label="Увеличить счетчик" onClick={increment}>
    Increment
  </button>
);

Заключение

Подводя итог: Виртуальный DOM в React — это замечательная особенность, которая значительно повышает производительность ваших веб-приложений. Создавая легковесную версию реального DOM, React может эффективнее выполнять обновления, избегая замедлений, связанных с прямым манипулированием DOM.
Тем не менее, важно остерегаться распространенных проблем: избыточных перерисовок, плохого управления ключами в списках и путаницы между состоянием и пропсами. Следуя лучшим практикам — таким как использование мемоизации, контекста для управления состоянием и грамотная обработка побочных эффектов — вы сможете максимально эффективно использовать React и поддерживать плавную работу приложений.
Удачного кодинга!

Ресурсы

  1. https://www.geeksforgeeks.org/reactjs-virtual-dom/
  2. https://svelte.dev/blog/virtual-dom-is-pure-overhead
  3. https://refine.dev/blog/react-virtual-dom/#introduction