Устройство кодовой базы

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

Если вы хотите внести свой вклад в React, то мы надеемся, что эта глава вам поможет.

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

Внешние зависимости

У React практически нет внешних зависимостей. Обычно require() указывает на файл из собственной кодовой базы React. Однако есть несколько исключений.

Репозиторий fbjs существует, потому что React использует общий код с такими библиотеками как Relay. React не имеет зависимостей от модулей из экосистемы Node, для того, чтобы разработчики Facebook могли вносить изменения тогда, когда они им необходимы. fbjs не имеет внешнего API, а все его внутренние модули используются только проектами Facebook, например React.

Папки верхнего уровня

После клонирования репозитория React вы увидите папки верхнего уровня:

  • packages содержит метаданные (такие как package.json) и исходный код (подпапка src) для каждого пакета из репозитория React. Большая часть работы с кодом происходит в подпапках src внутри каждого пакета.
  • fixtures содержит несколько небольших React-приложений для контрибьютеров.
  • build содержит скомпилированную версию React. Этого каталога нет в репозитории, но он появится после того, как вы соберёте проект в первый раз.

Документация расположена в отдельном репозитории.

Существуют ещё несколько вспомогательных верхнеуровневых папок. Но вы, вероятно, не столкнётесь с ними при изменении кода.

Тесты

У нас нет отдельной верхнеуровневой папки с юнит тестами. Вместо этого, мы помещаем их в папку __tests__, расположенную рядом с тестируемыми файлами.

Например, тесты для setInnerHTML.js расположены в __tests__/setInnerHTML-test.js.

Предупреждения и инварианты

Для работы с предупреждениями в React используется модуль warning.

var warning = require('warning');

warning(
  2 + 2 === 4,
  'Математика сегодня не работает.'
);

Предупреждения будут показаны, когда условие внутри warning равно false.

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

Использование дополнительной переменной поможет избежать спама в консоль.

var warning = require('warning');

var didWarnAboutMath = false;
if (!didWarnAboutMath) {
  warning(
    2 + 2 === 4,
    'Математика сегодня не работает.'
  );
  didWarnAboutMath = true;
}

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

var invariant = require('invariant');

invariant(
  2 + 2 === 4,
  'Ты не пройдёшь!'
);

Если условие внутри invariant равно false, тогда выбрасывается исключение.

«Инварианты» — это всего лишь способ сказать: «Условие всегда истинно». Вы можете думать о них как об утверждениях (assertion).

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

Режимы разработки и продакшена

Вы можете использовать псевдоглобальную переменную __DEV__, чтобы блок кода присутствовал только в режиме разработки.

На этапе компиляции добавится проверка process.env.NODE_ENV !== 'production', определяющая, должен ли данный блок кода присутствовать в сборке CommonJS.

Выражение будет равно true в неминифицированной сборке, а в минифицированном будет вырезан весь блок вместе с if.

if (__DEV__) {
  // Это код будет выполнен только в режиме разработки.
}

Flow

Недавно мы представили статический анализатор Flow. Файлы, помеченные аннотацией @flow в заголовке после лицензии, будут подвергнуты проверке типов.

Мы принимаем пулл реквесты на дополнение аннотаций Flow в уже существующий код. Вот пример кода:

ReactRef.detachRefs = function(
  instance: ReactInstance,
  element: ReactElement | string | number | null | false,
): void {
  // ...
}

Новый код, если это возможно, должен искользовать Flow. Вы можете выполнить yarn flow, чтобы произвести проверку типов.

Динамические инъекции

React использует динамические инъекции в некоторых модулях. Несмотря на то, что они явные, это всё ещё неудачное решение, потому что затрудняет понимание кода. Причина их существования в том, что изначально React поддерживал только DOM. React Native появился как форк React. Мы должны добавлять динамические инъекции, чтобы позволить React Native переопределять некоторое поведение.

Ниже вы можете увидеть, как в модулях объявляются динамические зависимости:

// Инжектится динамически
var textComponentClass = null;

// Зависит от инжектируемого значения
function createInstanceForText(text) {
  return new textComponentClass(text);
}

var ReactHostComponent = {
  createInstanceForText,

  // Позволяет использовать динамические инъекции
  injection: {
    injectTextComponentClass: function(componentClass) {
      textComponentClass = componentClass;
    },
  },
};

module.exports = ReactHostComponent;

Поле injection нигде не используется. Но по соглашению оно означает, что модуль может иметь зависимости (предположительно, платформо-зависимые), которые будут подключаться во время выполнения.

В репозитории существует много мест с использованием инъекций. В будущем мы хотим избавиться от механизма динамических инъекций и соединять все фрагменты статически во время сборки.

Множество пакетов

React является монолитным репозиторием. Он содержит множество отдельных пакетов, чтобы изменения были согласованными, а проблемы решались в одном месте.

Ядро React

Ядро включает в себя весь верхнеуровневый API, например:

  • React.createElement()
  • React.Component
  • React.Children

Ядро включает в себя только API необходимый для объявления компонентов. Оно не включает алгоритм согласования или какой-либо платформо-специфический код. Этот код находится в компонентах React DOM и React Native.

Код ядра расположен в папке packages/react. Он доступен в npm в виде пакета react. Соответствующая сборка для браузера экспортирует глобальную переменную React и называется react.js.

Рендереры

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

Рендереры превращают React дерево в платформо-специфический код.

Они расположены в каталоге packages/:

  • React DOM Renderer рендерит React-компоненты в DOM. Он реализует ReactDOM API и доступен как пакет react-dom из npm репозитория. Можно подключать как отдельный бандл react-dom.js, экспортирующий глобальную переменную ReactDOM.
  • React Native Renderer рендерит React компоненты в нативные представления. Используется внутри React Native.
  • React Test Renderer рендерит React компоненты в JSON-дерево. Используется при тестировании снимками через фреймворк Jest и доступен как пакет react-test-renderer в npm.

Мы начали поддерживать единственный неофициальный рендерер react-art, который раньше находился в отдельном GitHub-репозитории.

Примечание:

Технически react-native-renderer — это очень тонкий слой, который учит React взаимодействовать с React Native. Платформо-специфический код, управляющий нативными представлениями, расположен в репозитории React Native вместе с его компонентами.

Согласователи

Даже очень непохожим рендерерам, таким как React DOM и React Native, необходимо разделять много логики. В частности, реализации алгоритма согласования должны быть настолько похожими, чтобы декларативный рендеринг, пользовательские компоненты, состояние, методы жизненного цикла и рефы работали одинаково между различными платформами.

В качестве решения, различные рендереры имеют между собой общий код. В React мы называем эту часть «согласователь». Когда запланировано такое обновление, как setState(), согласователь вызывает render() в дереве компонентов и монтирует, обновляет либо размонтирует их.

Согласователи не являются отдельнымы пакетами, потому что не имеют открытого API. Вместо этого они используются исключительно такими рендерерами как React DOM и React Native.

Согласователь Stack

Согласователь «Stack» — это реализация, которая использовалась в React 15 и более ранних версиях. Мы перестали его использовать, но задокументировали в следующией главе.

Согласователь Fiber

В согласователе «Fiber» мы пытаемся исправить давно существующие ошибки и решить проблемы, появившиеся в согласователе Stack.

Его основными целями являются:

  • Разделение прерываемых задач на подзадачи.
  • Дать задачам приоритеты, иметь возможность перемещать их и переиспользовать.
  • Иметь возможность перемещаться вперёд и назад между родителями и детьми в разметке React.
  • Иметь возможность возвращать множество элементов из метода render().
  • Улучшенная обработка ошибок.

Вы можете узнать больше об архитектуре React Fiber здесь и здесь. Несмотря на то, что он включён в React 16, асинхронная функциональность по умолчанию не включена.

Исходный код расположен в папке packages/react-reconciler.

Система событий

В React реализована система синтетических событий, которая не зависит от используемого рендерера, и работает одинаково как React DOM, так и в React Native. Исходный код расположен в каталоге packages/react-events.

Больше подробностей вы можете узнать из видео (66 mins).

Что дальше?

В следующей главе вы познакомитесь с реализацией согласователя (версий React 15 и ранее) больее подробно. Для нового согласователя мы ещё не писали документацию.