Создай свой React за 5 шагов: полное руководство по архитектуре фреймворков 2024

Создай свой React за 5 шагов: полное руководство по архитектуре фреймворков 2024

1. Почему React победил: секреты самого популярного фреймворка

Когда React только появился, многие разработчики отнеслись к нему скептически. Идея смешивать HTML и JavaScript (JSX) казалась шагом назад. Однако сегодня архитектура React стала стандартом индустрии. Давайте разберёмся, почему этот подход оказался настолько эффективным и вытеснил старые методы работы с DOM.

Главная причина успеха — смена парадигмы с императивной на декларативную.

В императивном подходе (как в jQuery) вы говорите браузеру, как что-то сделать: "Найди элемент с ID 'button', добавь ему класс 'active', измени текст на 'Loading'". Это приводит к запутанному коду, где состояние интерфейса размазано по всему приложению.

Frontend фреймворк React предлагает декларативный подход: вы описываете, что хотите видеть на экране в зависимости от состояния. "Если идет загрузка, кнопка должна быть серой и с текстом 'Loading'". React сам вычисляет минимально необходимые изменения в DOM, чтобы привести интерфейс к нужному виду.

Ключевые преимущества такой архитектуры:

  • Компонентная модель. Интерфейс разбивается на независимые кирпичики. Это упрощает тестирование и переиспользование кода.
  • Однонаправленный поток данных. Данные текут от родителя к ребенку через props. Это делает отладку предсказуемой: вы всегда знаете, откуда пришли данные.
  • Экосистема. React — это не просто библиотека, это платформа, вокруг которой выросли инструменты вроде Next.js, React Native и тысячи библиотек компонентов.

В следующих разделах мы заглянем "под капот" этой технологии и узнаем, как именно магия React превращает JavaScript-объекты в живой интерфейс ↓

2. Сердце системы: как Virtual DOM решает проблему производительности

Сравнение Real DOM и Virtual DOM: как React оптимизирует обновления.
Сравнение Real DOM и Virtual DOM: как React оптимизирует обновления.

Самая дорогая операция в браузере — это манипуляции с реальным DOM-деревом. Каждый раз, когда вы меняете элемент, браузеру нужно пересчитать стили, макет (layout) и перерисовать часть страницы (paint). Если делать это часто и хаотично, приложение начнет тормозить.

Здесь на сцену выходит Virtual DOM.

Virtual DOM — это легковесная копия реального DOM, хранящаяся в памяти в виде обычных JavaScript-объектов. Когда состояние компонента меняется, React не бежит сразу менять реальный DOM. Вместо этого он:

  1. Создает новое дерево Virtual DOM.
  2. Сравнивает его с предыдущей версией (процесс называется Diffing).
  3. Вычисляет минимальный набор изменений.
  4. Вносит эти изменения в реальный DOM за один раз.

Посмотрим, как выглядит элемент Virtual DOM на уровне кода. Это не магия, а простая структура данных:

const element = {
  type: "h1",
  props: {
    title: "Hello World",
    children: "Welcome to my blog"
  }
};

Когда вы пишете JSX, он трансформируется именно в такие объекты. Если в реальном DOM у элемента div есть сотни свойств (методы событий, стили, атрибуты), то в Virtual DOM мы храним только то, что нам действительно нужно.

Сравнение двух таких объектов происходит молниеносно по сравнению с чтением реального DOM. Именно этот механизм позволяет React обновлять интерфейс с частотой 60 кадров в секунду даже в сложных приложениях.

Курс AI для разработчиков. Увеличиваем производительность разработчиков за счет внедрения AI-инструментов.

3. Компоненты и Хуки: управление состоянием без боли

Долгое время в React существовало разделение: классовые компоненты для сложной логики и состояния, функциональные — только для отрисовки. С появлением React Hooks в версии 16.8 ситуация кардинально изменилась. Теперь функции стали полноправными владельцами состояния.

Но как обычная JavaScript-функция может "помнить" свое состояние между вызовами?

Функциональный компонент в React — это просто функция, которая принимает props и возвращает Virtual DOM. Когда React рендерит компонент, он вызывает эту функцию. Переменные внутри функции создаются заново при каждом вызове.

Хуки работают благодаря механизму замыканий и тому факту, что React хранит порядок вызова хуков. Давайте представим упрощенную модель того, как React хранит состояние для компонента:

  • У React есть внутренний массив для хранения состояний каждого компонента.
  • Есть индекс (курсор), указывающий на текущий хук.
  • При первом рендере React записывает начальное значение в массив.
  • При повторном рендере он берет значение из массива по текущему индексу и инкрементирует индекс.

Именно поэтому существует строгое правило: нельзя вызывать хуки внутри циклов или условий. Порядок вызовов должен быть одинаковым при каждом рендере, иначе React "потеряет", какому useState принадлежит значение из внутреннего массива.

Этот подход позволил:

  • Избавиться от громоздкого this.
  • Легко переиспользовать логику между компонентами (кастомные хуки).
  • Группировать код по смыслу (например, подписка и отписка в useEffect), а не по методам жизненного цикла.

4. Механизм рендеринга: от JSX до пикселей на экране простыми словами

Путь рендеринга: от JSX-кода до финальных пикселей на экране.
Путь рендеринга: от JSX-кода до финальных пикселей на экране.

Чтобы создать свой React, нужно понимать цепочку превращений, которая происходит с вашим кодом. Путь от JSX до пикселей состоит из нескольких этапов.

Этап 1: Транспиляция (JSX -> JS)

Браузеры не понимают JSX. Инструменты вроде Babel превращают теги в вызовы функций.

Код разработчика:

const element = <h1 title="foo">Hello</h1>;

Код после Babel:

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
);

Этап 2: Создание элементов (JS -> VDOM)

Функция createElement не делает ничего сложного. Она просто возвращает объект, описывающий элемент. Мы уже видели его структуру: type и props. Это и есть узел Virtual DOM.

Этап 3: Согласование (Reconciliation)

На этом этапе React строит дерево "Файберов" (Fibers). Fiber — это единица работы в React. В отличие от простого дерева компонентов, структура Fiber позволяет React приостанавливать и возобновлять рендеринг, чтобы не блокировать основной поток браузера.

Каждый Fiber-узeл связан с:

  • child (первый дочерний элемент)
  • sibling (следующий братский элемент)
  • parent (родительский элемент)

Эта структура данных (связный список) позволяет обходить дерево, останавливаться и продолжать с того же места.

Этап 4: Коммит (Commit Phase)

Когда все вычисления закончены и дерево изменений готово, React переходит к фазе коммита. На этом этапе библиотека берет накопленные изменения и применяет их к реальному DOM: создает узлы document.createElement, добавляет атрибуты и вставляет их в страницу.

Теперь, когда мы понимаем теорию, давайте перейдем к практике ↓

5. Практика: собираем минимальный React-аналог на чистом JavaScript

Мы напишем свою мини-библиотеку, которая сможет рендерить компоненты в браузере. Мы не будем стремиться к полному соответствию API React 19, но реализуем ключевую архитектуру.

Шаг 1: Функция createElement

Начнем с функции, которая создает объекты Virtual DOM. Назовем нашу библиотеку MiniReact.

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // Дети могут быть либо объектами (другие элементы), 
      // либо примитивами (строки, числа).
      // Для единообразия обернем примитивы в специальный тип.
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXTELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

Теперь мы можем заменить React.createElement на нашу функцию.

Шаг 2: Функция render

Нам нужна функция, которая превратит наши объекты в реальные DOM-узлы. Для начала реализуем упрощенную версию без конкурентного режима (Concurrent Mode), которая строит дерево рекурсивно.

function render(element, container) {
// 1. Создаем DOM-узeл в зависимости от типа
const dom =
element.type === "TEXTELEMENT"
? document.createTextNode("")
: document.createElement(element.type);

// 2. Назначаем свойства (props) узлу
const isProperty = key => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name];
});

// 3. Рекурсивно рендерим детей
element.props.children.forEach(child =>
render(child, dom)
);

// 4. Добавляем узел в контейнер
container.appendChild(dom);
}

На этом этапе у нас уже есть рабочая библиотека, способная отрисовать статический контент!

Шаг 3: Введение в Fiber-архитектуру

Рекурсивный рендер, который мы написали выше, имеет проблему: если дерево элементов очень большое, рекурсия заблокирует основной поток браузера, пока не отрисует всё до конца. Интерфейс "зависнет".

Современный React решает это, разбивая работу на мелкие части (Unit of Work). Для этого используется структура данных Fiber.

Вместо рекурсии мы будем использовать цикл и структуру связного списка. Каждый элемент будет знать о своем родителе, первом ребенке и соседе.

Давайте опишем алгоритм создания Fiber-дерева (упрощенно):

  1. Мы начинаем с корневого элемента.
  2. Для каждого элемента мы создаем Fiber-узел.
  3. Добавляем элемент в DOM (но пока не показываем пользователю).
  4. Ищем следующую задачу:
    • Если есть child, идем к нему.
    • Если нет child, идем к sibling (брату).
    • Если нет ни того, ни другого, возвращаемся к parent и ищем его братьев.

Шаг 4: Реализация Work Loop (Цикл работы)

Чтобы не блокировать браузер, нам нужен механизм, который выполняет работу кусочками. В браузере есть API, позволяющие планировать выполнение кода, когда главный поток свободен.

Мы создадим переменную nextUnitOfWork, которая будет хранить ссылку на следующий Fiber, который нужно обработать.

let nextUnitOfWork = null;

function workLoop(deadline) {
  let shouldYield = false;
  
  // Пока есть работа и браузер дает нам время
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    
    // Проверяем, сколько времени осталось
    // (в реальном коде здесь используется более сложный планировщик)
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  // Планируем следующий запуск
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

Примечание: В реальном React вместо requestIdleCallback сейчас используется собственный планировщик (Scheduler package), так как поведение нативных API может различаться в разных браузерах, но принцип разделения работы остается тем же.

Шаг 5: Сборка и запуск

Теперь, когда у нас есть createElement и понимание процесса рендеринга, мы можем собрать всё вместе.

const MiniReact = {
  createElement,
  render,
};

/** @jsx MiniReact.createElement */
const element = (
  <div style="background: #f0f0f0; padding: 20px;">
    <h1>Привет, мир!</h1>
    <p>Мы создали свой собственный рендерер.</p>
  </div>
);

const container = document.getElementById("root");
MiniReact.render(element, container);

Если вы подключите этот скрипт и настроите Babel для обработки JSX с помощью нашей функции MiniReact.createElement, вы увидите результат на экране.

Итоги и что дальше

Мы с вами прошли путь от понимания философии React до написания собственного минимального рендерера. Конечно, наш MiniReact еще далек от оригинала. В настоящем React есть:

  • Reconciliation (Сверка): сложный алгоритм сравнения старого и нового дерева Fiber для эффективного обновления, а не полной перерисовки.
  • Синтетические события: кроссбраузерная обертка над нативными событиями DOM.
  • Планировщик приоритетов: React умеет отличать важные обновления (анимация, ввод текста) от второстепенных (загрузка данных).

Однако понимание того, как работает createElement, что такое Virtual DOM и зачем нужна Fiber-архитектура, делает вас гораздо более осознанным разработчиком. Вы перестаете смотреть на React как на "черный ящик" и начинаете понимать, почему нельзя мутировать стейт напрямую или зачем нужны ключи в списках.

Теперь вы готовы углубляться в изучение исходного кода настоящего React и писать более эффективные приложения.