1. Host Tree
2. Host Instances
3. Renderers
4. React Elements
5. Entry Point
6. Reconciliation
7. Conditions

Большинство руководств представляют React как библиотеку пользовательского интерфейса. Это имеет смысл, потому что React — это библиотека пользовательского интерфейса. Это буквально то, что говорит слоган!

Это глубокое погружение — это пост, который не подходит для новичков. В этом посте я описываю большую часть модели программирования React с первых принципов. Я не объясняю, как его использовать — просто как он работает. Он предназначен для опытных программистов и людей, работающих над другими библиотеками пользовательского интерфейса, которые задавали вопросы о некоторых компромиссах в React.

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

1. Host Tree

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

Программы React обычно выводят дерево, которое со временем может измениться. Это может быть дерево DOM, иерархия iOS, дерево примитивов PDF или даже объектов JSON. Однако обычно мы хотим представить с его помощью некоторый пользовательский интерфейс. Мы назовем это «деревом хоста» (host tree), потому что оно является частью хост-среды вне React — как DOM или iOS. Дерево хостов обычно имеет собственный императивный API. React — это слой поверх него.

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

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

  • Стабильность. Дерево хостов относительно стабильно, и большинство обновлений не меняют радикально его общую структуру. Если бы приложение каждую секунду переставляло все свои интерактивные элементы в совершенно другую комбинацию, им было бы сложно пользоваться. Куда пропала эта кнопка? Почему мой экран танцует?

  • <li><b>Регулярность</b>. Дерево узлов можно разбить на шаблоны пользовательского интерфейса, которые выглядят и ведут себя последовательно (например, кнопки, списки, аватары), а не случайные формы. В реакт это принято называть композицией. Композиция это когда вы используете один и тот же кусок интерфейса в разныъ частях сайта.</li>

Эти принципы справедливы для большинства пользовательских интерфейсов. Однако React плохо подходит, когда на выходе нет стабильных «паттернов». Например, React может помочь вам написать клиент для Twitter, но не будет очень полезен для заставки с 3D-трубами.

2. Host Instances

Дерево хостов состоит из узлов. Мы назовем их «хост-экземпляры» (Host Instances).

В среде DOM хост-экземпляры являются обычными узлами DOM — подобно объектам, которые вы получаете, когда вызываете document.createElement('div'). В iOS экземпляры узла могут быть значениями, однозначно идентифицирующими собственное представление из JavaScript.

Экземпляры хоста имеют свои собственные свойства (например, domNode.className или view.tintColor). Они также могут содержать другие экземпляры хоста в качестве дочерних.

(Это не имеет ничего общего с React — я описываю среду хоста.)

Обычно существует API для управления экземплярами хоста. Например, DOM предоставляет такие API, как appendChild, removeChild, setAttribute и т. д. В приложениях React вы обычно не вызываете эти API. Это работа React.

Вам не нужно использовать этот низкоуровневый API (вроде document.createElement('div')), вместо этого React предостовляет вам удобные неизменные абстракции для работы над пользовательскими интерфейсами.

3. Renderers

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

Рендереры React могут работать в одном из двух режимов.

Подавляющее большинство рендереров написано для использования режима «мутации». В этом режиме работает DOM: мы можем создать узел, установить его свойства, а затем добавить или удалить из него дочерние узлы. Экземпляры хоста полностью изменяемы.

React также может работать в «постоянном» режиме. Этот режим предназначен для хост-сред, которые не предоставляют такие методы, как appendChild(), а вместо этого клонируют родительское дерево и всегда заменяют дочерний элемент верхнего уровня. Неизменяемость на уровне дерева хостов упрощает многопоточность. React Fabric использует это преимущество.

Как пользователю React, вам не нужно думать об этих режимах. Я только хочу подчеркнуть, что React — это не просто переходник из одного режима в другой. Его полезность ортогональна парадигме целевого низкоуровневого API-интерфейса.

4. React Elements

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

Элемент React — это простой объект JavaScript. Он может описывать хост-экземпляр.

// JSX это синтаксический сахар для этих объектов
// 
{
  type: 'button',
  props: { className: 'blue' }
}

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

Как и экземпляры хоста, элементы React могут формировать дерево:

// JSX это синтаксический сахар для этих объектов.
// 
//   
//   
// 
{
  type: 'dialog',
  props: {
    children: [{
      type: 'button',
      props: { className: 'blue' }
    }, {
      type: 'button',
      props: { className: 'red' }
    }]
  }
}

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

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

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

5. Entry Point

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

Например, точка входа React DOM — ReactDOM.render:

ReactDOM.render(
  // { type: 'button', props: { className: 'blue' } }
  ,
  document.getElementById('container')
);

Когда мы говорим ReactDOM.render(reactElement, domContainer), мы имеем в виду: «Дорогой React, сделай так, чтобы дерево узлов domContainer соответствовало моему reactElement».

React просмотрит reactElement.type (в нашем примере это «button») и попросит средство визуализации React DOM создать для него хост-экземпляр и установить свойства:

// Где-то в рендерере ReactDOM (упрощенно)
function createHostInstance(reactElement) {
  let domNode = document.createElement(reactElement.type);
  domNode.className = reactElement.props.className;
  return domNode;
}

В нашем примере эффективно React сделает это:

let domNode = document.createElement('button');
domNode.className = 'blue';

domContainer.appendChild(domNode);

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

6. Reconciliation

Что произойдет, если мы дважды вызовем ReactDOM.render() с одним и тем же контейнером?

ReactDOM.render(
  ,
  document.getElementById('container')
);
// ... потом ...
// Должен ли он заменять хост-экземпляр кнопки
// или просто обновлять свойство существующего?
ReactDOM.render(
,
document.getElementById('container')
);

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

Есть два способа сделать это. Упрощенная версия React может снести существующее дерево и воссоздать его с нуля:

let domContainer = document.getElementById('container');
// Очистить дерево
domContainer.innerHTML = '';
// Создайте новое дерево экземпляров хоста
let domNode = document.createElement('button');
domNode.className = 'red';
domContainer.appendChild(domNode);

Но в DOM это происходит медленно и теряет важную информацию, такую как фокус, выделение, состояние прокрутки и т. д. Вместо этого мы хотим, чтобы React делал что-то вроде этого:

let domNode = domContainer.firstChild;
// Обновить существующий хост-экземпляр
domNode.className = 'red';

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

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

В нашем примере все просто. Раньше мы отображали button как первого (и единственного) дочернего элемента, и мы хотим снова отобразить buttonв том же месте. У нас уже есть хост-экземпляр button, так зачем его создавать заново? Давайте просто использовать его повторно.

Это довольно близко к тому, как React думает об этом.

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

Вот пример с комментариями, примерно показывающий, что делает React:

// let domNode = document.createElement('button');
// domNode.className = 'blue';
// domContainer.appendChild(domNode);
ReactDOM.render(
  ,
  document.getElementById('container')
);

// Можно ли повторно использовать хост-экземпляр? Да! (button → button)
// domNode.className = 'red';
ReactDOM.render(
  ,
  document.getElementById('container')
);

// Можно ли повторно использовать хост-экземпляр? Нет! (button → p)
// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
  Hello,
  document.getElementById('container')
);

// Можно ли повторно использовать хост-экземпляр? Да! (p → p)
// domNode.textContent = 'Goodbye';
ReactDOM.render(
  Goodbye,
  document.getElementById('container')
);

Та же эвристика используется для дочерних деревьев. Например, когда мы обновляем dialog с двумя button внутри, React сначала решает, использовать ли повторно dialog, а затем повторяет эту процедуру принятия решения для каждого дочернего элемента.

7. Conditions

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

Скажем, мы хотим сначала показать только ввод, а затем отобразить сообщение перед ним:

// Первый рендер
ReactDOM.render(
  
    
  ,
  domContainer
);

// Следующий рендер
ReactDOM.render(
  
    I was just added here!
    
  ,
  domContainer
);

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

  • dialog → dialog: можно ли повторно использовать хост-экземпляр? Да — тип совпадает.

  • input → p: Можно ли повторно использовать хост-экземпляр? Нет, тип изменился! Необходимо удалить существующий ввод и создать новый экземпляр хоста p.

  • (nothing) → input: необходимо создать новый экземпляр узла ввода.

Таким образом, код обновления, выполняемый React, будет выглядеть так:

let oldInputNode = dialogNode.firstChild;
dialogNode.removeChild(oldInputNode);

let pNode = document.createElement('p');
pNode.textContent = 'I was just added here!';
dialogNode.appendChild(pNode);

let newInputNode = document.createElement('input');
dialogNode.appendChild(newInputNode);

Это не очень хорошо, потому что концептуально input не был заменен на p — он просто переместился. Мы не хотим потерять его выделение, состояние фокуса и содержимое из-за повторного создания DOM.

Хотя у этой проблемы есть простое решение (о котором мы поговорим через минуту), в приложениях React она возникает нечасто. Интересно посмотреть, почему.

На практике вы редко будете вызывать ReactDOM.render напрямую. Вместо этого приложения React, как правило, разбиваются на такие функции:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = I was just added here!;
  }
  return (
    
      {message}
      
    
  );
}

Этот пример не страдает от проблемы, которую мы только что описали. Было бы легче понять, почему, если бы мы использовали нотацию объектов вместо JSX. Посмотрите на дерево дочерних элементов диалога:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = {
      type: 'p',
      props: { children: 'I was just added here!' }
    };
  }
  return {
    type: 'dialog',
    props: {
      children: [
        message,
        { type: 'input', props: {} }
      ]
    }
  };
}

Независимо от того, является ли showMessage true или false, input является вторым дочерним элементом и не меняет свою позицию в дереве между рендерингами.

Комментарии (0)