[Перевод] React как среда выполнения пользовательского интерфейса

внешний интерфейс React.js

Оригинальный адрес:слишком остро отреагировал.IO/реагировать-как-ах-…

Оригинальный автор:Dan Abramov

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

React homepage screenshot:

Я когда-то писал о строительствеПользовательский интерфейсПроблемы, с которыми придется столкнуться. Но в этой статье мы поговорим о React по-другому — потому что это скореевремя выполнения программирования.

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


Примечание. Если вы все еще изучаете React, перейдите кофициальная документацияучиться

⚠️.

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

Эта статья предназначена для опытных программистов и тех, кто использовал другие UI-библиотеки, но в итоге выбрал React, взвесив все за и против в своем проекте, надеюсь, она вам поможет!

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

Без лишних слов, давайте начнем глубже разбираться в React!


хост-дерево

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

Программы React обычно выводятДерево, которое меняется со временем.это может бытьDOM-дерево,Просмотр слоя iOS,PDF-примитивы, илиJSON-объект. Однако обычно мы хотим использовать его для отображения UI. Мы называем это «деревом-хозяином», потому что он имеет тенденцию быть частью хост-среды вне реагирования - как DOM или IOS. Хозяева обычно имеетЭтоСобственныйимперативный API. А React — это слой поверх него.

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

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

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

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

Хост-экземпляр

Дерево узлов состоит из узлов, которые мы называем «экземпляр узла».

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

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

(На это нечего реагировать — потому что я говорю хост-среде.)

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

Рендерер

Renderer Church React Как взаимодействовать с конкретными хост-средами и как управлять их хост-экземпляром. Реагировать Дом, Реагировать НативInkОба можно назвать рендерерами React. вы также можетеСоздайте свой собственный рендерер React.

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

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

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

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

Реагировать на элементы

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

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

// JSX 是用来描述这些对象的语法糖。
// <button className="blue" />
{
  type: 'button',
  props: { className: 'blue' }
}

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

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

// JSX 是用来描述这些对象的语法糖。
// <dialog>
//   <button className="blue" />
//   <button className="red" />
// </dialog>
{
  type: 'dialog',
  props: {
    children: [{
      type: 'button',
      props: { className: 'blue' }
    }, {
      type: 'button',
      props: { className: 'red' }
    }]
  }
}

(Примечание: я опустил некоторые из них, которые не важны для этого объяснения.Атрибуты)

Однако, пожалуйста, помнитеЭлементы React не существуют вечно. Они всегда находятся в постоянном цикле между восстановлением и удалением.

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

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

Вход

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

Например, вход React DOMReactDOM.render:

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

когда мы звонимReactDOM.render(reactElement, domContainer)Когда мы имеем в виду:"Дорогой React, поставь мойreactElementсопоставить сdomContaienrподняться по дереву хостов. "

Реакция будет смотреть на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 будет рекурсивно создавать для них хост-экземпляры при первом рендеринге.

координация

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

ReactDOM.render(
  <button className="blue" />,
  document.getElementById('container')
);

// ... 之后 ...

// 应该替换掉 button 宿主实例吗?
// 还是在已有的 button 上更新属性?
ReactDOM.render(
  <button className="red" />,
  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>Почему воссоздать это? Давайте повторно повторно повторно.

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

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

Вот пример с комментариями, примерно объясняющими, как работает React:

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

// 能重用宿主实例吗?能!(button → button)
// domNode.className = 'red';
ReactDOM.render(
  <button className="red" />,
  document.getElementById('container')
);

// 能重用宿主实例吗?不能!(button → p)
// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <p>Hello</p>,
  document.getElementById('container')
);

// 能重用宿主实例吗?能!(p → p)
// domNode.textContent = 'Goodbye';
ReactDOM.render(
  <p>Goodbye</p>,
  document.getElementById('container')
);

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

условие

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

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

// 第一次渲染
ReactDOM.render(
  <dialog>
    <input />
  </dialog>,
  domContainer
);

// 下一次渲染
ReactDOM.render(
  <dialog>
    <p>I was just added here!</p>
    <input />
  </dialog>,
  domContainer
);

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

  • dialog → dialog: Можно ли повторно использовать хост-экземпляр?Может — потому что типы совпадают.
    • input → p: Можно ли повторно использовать хост-экземпляр?Нет, тип изменился!Необходимо удалить существующиеinputзатем воссоздатьpЭкземпляр хоста.
    • (nothing) → input: нужно воссоздать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 = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

Этот пример не страдает от проблем, которые мы только что описали. Давайте использовать объектные аннотации вместо JSX, чтобы, возможно, лучше понять, почему. посмотриdialogДерево подэлементов в:

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>Всегда в положении второго ребенка и не изменится.

еслиshowMessageотfalseизменить наtrue, React обходит все дерево элементов и сравнивает его с предыдущей версией:

  • dialog → dialog: Можно ли повторно использовать хост-экземпляр?Может - потому что типы совпадают.
    • (null) → p: Нужно вставить новыйpЭкземпляр хоста.
    • input → input: Можно ли повторно использовать хост-экземпляр?Может - потому что типы совпадают.

Затем React выполнит код примерно так:

let inputNode = dialogNode.firstChild;
let pNode = document.createElement('p');
pNode.textContent = 'I was just added here!';
dialogNode.insertBefore(pNode, inputNode);

Таким образом, состояние поля ввода не будет потеряно.

список

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

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

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

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

Если бы наш список продуктов был переупорядочен, React увидел бы только всеpи внутриinputимеют тот же тип и не знаю, как их перемещать. (С точки зрения React, хотя сами элементы изменились, их порядок не изменился.)

Таким образом, React переупорядочивает десять элементов следующим образом:

for (let i = 0; i < 10; i++) {
  let pNode = formNode.childNodes[i];
  let textNode = pNode.firstChild;
  textNode.textContent = 'You bought ' + items[i].name;
}

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

Вот почему каждый раз, когда вывод содержит массив элементов, React позволит вам указать массив с именемkeyсвойства:

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p key={item.productId}>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

keyДает React возможность определить, действительно ли дочерний элемент такой же, даже если его положение в родительском элементе не совпадает до и после рендеринга.

Когда Реакт<form>найти в<p key="42">, он проверяет<form>Содержит ли он также<p key="42">. хотя<form>Этот метод также работает после дочерних элементов в файле . Когда ключ остается одним и тем же до и после рендеринга, React повторно использует предыдущий экземпляр хоста, а затем меняет порядок своих братьев и сестер.

должен быть в курсеkeyТолько связанный с определенным родительским элементом React, например<form>. React не сопоставляет дочерние элементы с разными родительскими элементами, но с одним и тем же ключом. (В React нет идиоматической поддержки перемещения экземпляра хоста между разными родительскими элементами без повторного создания элемента.)

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

компоненты

Мы уже знаем, что функции возвращают элементы React:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

Эти функции называются компонентами. Они позволяют нам создавать собственные «инструменты», такие как кнопки, аватары, поля для комментариев и многое другое. Компоненты — это хлеб с маслом React.

Компонент принимает один параметр — хэш объекта. Он содержит «реквизит» (сокращение от «свойства»). это здесьshowMessageэто просто реквизит. Они похожи на именованные параметры.

чистый

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

function Button(props) {
  // 🔴 没有作用
  props.isActive = true;
}

Вообще говоря, мутация не идиоматическая в реакции. (Мы объясним, как обновить UI в ответ на события более идиоматическими способами позже.)

Тем не менее, местные мутации абсолютно разрешены:

function FriendList({ friends }) {
  let items = [];
  for (let i = 0; i < friends.length; i++) {
    let friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    );
  }
  return <section>{items}</section>;
}

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

Точно так же разрешена ленивая инициализация, даже если она не совсем «чистая»:

function ExpenseForm() {
  // 只要不影响其他组件这是被允许的:
  SuperCalculator.initializeIfNotReady();

  // 继续渲染......
}

React не волнует, является ли ваш код на 100% чистым, как строго функциональное программирование, если можно безопасно вызывать компонент несколько раз и не влиять на рендеринг других компонентов. В РеакцииидемпотентностьВажнее чистоты.

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

рекурсия

Как мы можем использовать компоненты в компонентах? Компоненты — это функции, поэтому мы можем вызывать их напрямую:

let reactElement = Form({ showMessage: true });
ReactDOM.render(reactElement, domContainer);

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

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

// { type: Form, props: { showMessage: true } }
let reactElement = <Form showMessage={true} />;
ReactDOM.render(reactElement, domContainer);

Тогда внутри реагирования ваш компонент будет называться таким:

// React 内部的某个地方
let type = reactElement.type; // Form
let props = reactElement.props; // { showMessage: true }
let result = type(props); // 无论 Form 会返回什么

Имена функций компонентов пишутся с заглавной буквы, как указано. Видно, когда JSX конвертируется<Form>вместо<form>, что делает объектtypeсам становится идентификатором, а не строкой:

console.log(<form />.type); // 'form' 字符串
console.log(<Form />.type); // Form 函数

У нас нет глобального механизма регистрации — буквально, когда мы набираем<Form>время представляетForm. еслиFormне существует в локальной области видимости, и вы обнаружите ошибку JavaScript, как обычно с неправильным именем переменной.

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

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

  • ты: ReactDOM.render(<App />, domContainer)
  • Реагировать: App, что вы хотите отрендерить?
    • App: я хочу отображать содержит<Content>из<Layout>.
  • Реагировать: <Layout>, что вы рендерите?
    • Layout: Я бы хотел<div>Визуализацию моих детей в. мой ребенок элемент<Content>Так что я думаю, он должен оказывать<div>входить.
  • Реагировать: <Content>, что вы рендерите?
    • <Content>: Я бы хотел<article>визуализировать некоторый текст в и<Footer>.
  • Реагировать: <Footer>, что вы рендерите?
    • <Footer>: я хочу визуализировать<footer>.
  • Реагировать:Хорошо, приступим:
// 最终的 DOM 结构
<div>
  <article>
    Some text
    <footer>some more text</footer>
  </article>
</div>

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

Здесь применяются те же критерии гармонизации, которые обсуждались ранее. если в том же местеtypeизменено (по индексу и необязательноkeyрешение), React удаляет в нем хост-экземпляр и перестраивает его.

Инверсия контроля

Вам может быть интересно: почему бы нам просто не вызвать компонент напрямую? зачем писать<Form />вместоForm()?

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

// 🔴 React 并不知道 Layout 和 Article 的存在。
// 因为你在调用它们。
ReactDOM.render(
  Layout({ children: Article() }),
  domContainer
)

// ✅ React知道 Layout 和 Article 的存在。
// React 来调用它们。
ReactDOM.render(
  <Layout><Article /></Layout>,
  domContainer
)

Это оИнверсия контроляклассический случай. Когда React вызывает наш компонент, мы получаем несколько интересных свойств:

  • Компоненты — это не просто функции.React может улучшать компоненты с помощью таких функций, как локальное состояние, которое тесно связано с самим компонентом в дереве. Хорошая среда выполнения предоставляет базовые абстракции, соответствующие решаемой задаче. Как мы уже упоминали, React предназначен для приложений, которые отображают деревья пользовательского интерфейса и реагируют на взаимодействия. Если вы вызываете компонент напрямую, вам придется создавать эти функции самостоятельно.
  • Типы компонентов участвуют в согласовании.Вызов вашего компонента через React позволяет ему больше узнать о структуре дерева элементов. Например, при рендеринге из<Feed>страница вProfileстранице, React не будет пытаться повторно использовать в ней хост-экземпляр — как вы бы сделали с<p>заменять<button>Такой же. Все состояние теряется — обычно это хорошо при рендеринге совершенно другого вида. ты не хочешь быть<PasswordForm>а также<MessengerChat>Состояние поля ввода сохраняется между<input>Позиции неожиданно «выстраиваются» между ними.
  • React может отсрочить согласование.Если вы позволите React управлять вашими компонентами, он может сделать много интересных вещей. Например, он может позволить браузеру выполнять некоторую работу между вызовами компонентов, чтобы при повторном рендеринге большого дерева компонентовНе блокирует основной поток. Было бы сложно организовать этот процесс вручную, не полагаясь на React.
  • Лучшая отладка.Если компонент является первоклассным гражданином, оцененным в библиотеке, мы можем построитьБогатые инструменты разработчика, для самоанализа в развитии.

Последним преимуществом того, что React вызывает функции вашего компонента, является ленивая оценка. Давайте посмотрим, что это значит.

ленивая оценка

Когда мы вызываем функцию в JavaScript, аргументы часто выполняются до вызова функции.

// (2) 它会作为第二个计算
eat(
  // (1) 它会首先计算
  prepareMeal()
);

Это обычно ожидается разработчиками JavaScript, потому что функции JavaScript могут иметь неявные побочные эффекты. Нас бы удивило, если бы мы вызвали функцию, а она не выполнялась до тех пор, пока ее результат не был каким-то образом «использован».

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

Рассмотрим следующее, которое содержит<Comments>из<Page>Компоненты:

function Story({ currentUser }) {
  // return {
  //   type: Page,
  //   props: {
  //     user: currentUser,
  //     children: { type: Comments, props: {} }
  //   }
  // }
  return (
    <Page user={currentUser}>
      <Comments />
    </Page>
  );
}

<Page>компоненты могут быть<Layout>отображает детей, переданных ему:

function Page({ currentUser, children }) {
  return (
    <Layout>
      {children}
    </Layout>
  );
}

(в JSX<A><B /></A>а также<A children={<B />} />такой же. )

Но что делать, если есть раннее возвращение?

function Page({ currentUser, children }) {
  if (!currentUser.isLoggedIn) {
    return <h1>Please login</h1>;
  }
  return (
    <Layout>
      {children}
    </Layout>
  );
}

Если мы вызовем как функциюCommonts(),Независимо от тогоPageНезависимо от того, хотите ли вы визуализировать их, они будут выполнены немедленно:

// {
//   type: Page,
//   props: {
//     children: Comments() // 总是调用!
//   }
// }
<Page>
  {Comments()}
</Page>

Но если мы передаем элемент React, нам не нужно делать это самим.Comments:

// {
//   type: Page,
//   props: {
//     children: { type: Comments }
//   }
// }
<Page>
  <Comments />
</Page>

Пусть React решит, когда и нужно ли вызывать компонент. если нашPageКомпонент игнорирует собственныеchildrenprop и вместо этого рендерит<h1>Please login</h1>, React не будет пытаться вызыватьCommentsфункция. В чем смысл?

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

условие

Ранее мы упоминали окоординацияИ как концептуальное «положение» элемента в дереве позволяет React узнать, следует ли повторно использовать хост-экземпляр или перестроить его. Экземпляр хоста может иметь все соответствующие локальные состояния: фокус, выбор, ввод и т. д. Мы хотим сохранить эти состояния при рендеринге обновлений концептуально идентичного пользовательского интерфейса. Мы также хотим уничтожить их предсказуемо, когда мы концептуально визуализируем что-то совершенно другое (например, из<SignupForm>Перевести в<MessengerChat>).

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

Мы называем эти функции хуками. Например,useStateПросто Крюк.

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Он возвращает пару значений: текущее состояние и функцию для обновления этого состояния.

массивдеструктурирующая грамматикаДавайте дадим переменным состояния собственные имена. Например, я называю их здесьcountа такжеsetCount, но их также можно назватьbananaа такжеsetBanana. Под этими словами мы будем использоватьsetStateчтобы заменить второе значение, как бы оно ни называлось в конкретном экземпляре.

(ты сможешьРеагировать на документациюСредняя школа узнает больше оuseStateи знание других хуков. )

последовательность

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

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

тайник

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

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

function Row({ item }) {
  // ...
}

export default React.memo(Row);

Теперь в родительском компоненте<Table>вызыватьsetStateкогда если<Row>серединаitemРезультат предыдущего рендера такой же, а React просто пропускает процесс согласования.

ты можешь пройтиuseMemo() HookПолучите детальный кеш на уровне отдельного выражения. Кэш привязан к связанному с ним компоненту и будет уничтожен вместе с локальным состоянием. Он сохранит только результат последнего вычисления.

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

оригинальная модель

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

Этот дизайн является преднамеренным. Для веб-приложенийвремя взаимодействияявляется ключевой метрикой, и настройка детальных слушателей путем обхода всей модели просто тратит драгоценное время. Кроме того, во многих приложениях взаимодействие приводит к обновлениям, которые либо малы (нажатие кнопки), либо велики (переходы страниц), поэтому мелкозернистые подписки просто тратят ресурсы памяти.

Один из принципов дизайна React заключается в том, что он может работать с необработанными данными. Если у вас есть набор объектов JavaScript, полученный из сетевого запроса, вы можете передать его напрямую компоненту без предварительной обработки. Нет вопросов о том, к каким свойствам можно получить доступ, или неожиданных скачков производительности при изменении структуры. Отрисовка React - это O (размер просмотра) вместо О(размер модели), и вы можете пройтиwindowingЗначительно уменьшает размер представления.

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

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

Массовое обновление

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

function Parent() {
  let [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      Parent clicked {count} times
      <Child />
    </div>
  );
}

function Child() {
  let [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Child clicked {count} times
    </button>
  );
}

Когда событие запускается, дочерний компонентonClickсрабатывает первым (при срабатывании егоsetState). затем родительский компонент в своем собственномonClickвызыватьsetState.

Если отреагируйте немедленно переиздание компонента ответитьsetStatecall, мы дважды перерисовываем дочерний компонент:

*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
  - setState
  - re-render Child // 😞 不必要的重渲染
Parent (onClick)
  - setState
  - re-render Parent
  - re-render Child
*** 结束 React 浏览器 click 事件处理过程 ***

первый разChildРендеринг компонентов является расточительным. И мы не позволим React пропуститьChildВторой рендерParentМожет передавать разные данные из-за собственного обновления состояния.

Вот почему обновления реактивных партий после всех событий в компоненте уволены:

*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
  - setState
Parent (onClick)
  - setState
*** Processing state updates                     ***
  - re-render Parent
  - re-render Child
*** 结束 React 浏览器 click 事件处理过程  ***

Вызов внутри компонентаsetStateПовторный рендеринг выполняется не сразу. Вместо этого React запускает все обработчики событий перед повторным рендерингом для того, что называется пакетным обновлением.

Пакетные обновления, хотя и полезны, могут вас удивить, если ваш код будет написан следующим образом:

  const [count, setCounter] = useState(0);

  function increment() {
    setCounter(count + 1);
  }

  function handleClick() {
    increment();
    increment();
    increment();
  }

если мы будемcountНачальное значение установлено на0, приведенный выше код будет представлять только три разаsetCount(1)передача. Для решения этой задачи даемsetStateВ качестве аргумента предоставляется функция «обновления»:

  const [count, setCounter] = useState(0);

  function increment() {
    setCounter(c => c + 1);
  }

  function handleClick() {
    increment();
    increment();
    increment();
  }

React поставит функции обновления в очередь и выполнит их по порядку после этого, в конце концовcountбудет установлен на3и в результате повторного рендеринга.

Когда логика состояния становится более сложной, чем просто несколькоsetStateПри звонке рекомендую использоватьuseReducer Hookчтобы описать ваше местное состояние. Это похоже на режим обновления «updater», где вы можете назвать каждое обновление:

  const [counter, dispatch] = useReducer((state, action) => {
    if (action === 'increment') {
      return state + 1;
    } else {
      return state;
    }
  }, 0);

  function handleClick() {
    dispatch('increment');
    dispatch('increment');
    dispatch('increment');
  }

actionПоля могут иметь любое значение, хотя чаще всего выбирают Object.

дерево вызовов

Среда выполнения языка программирования часто имеетстек вызовов. когда функцияa()передачаb(),b()позвони сноваc(), в движке JavaScript будет что-то вроде[a, b, c]Такая структура данных для «отслеживания» текущей позиции и того, какой код выполнять дальше. однаждыcКогда функция завершает выполнение, ее кадр стека вызовов исчезает! Потому что он больше не нужен. мы возвращаемся к функцииbсередина. когда мы закончим функциюa, стек вызовов очищается.

Конечно, React, работающий на JavaScript, также следует правилам JavaScript. Но мы можем представить себе наличие собственного стека вызовов внутри React, который запоминает компонент, который мы в данный момент визуализируем, например.[App, Page, Layout, Article /* 此刻的位置 */].

React отличается от языков программирования в обычном понимании тем, что он нацелен на отрисовку UI-деревьев, которые должны оставаться «живыми», чтобы мы могли с ними взаимодействовать. во-первыхReactDOM.render()Манипуляции с DOM не выполняются до тех пор, пока он не появится.

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

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

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

контекст

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

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

const ThemeContext = React.createContext(
  'light' // 默认值作为后备
);

function DarkApp() {
  return (
    <ThemeContext.Provider value="dark">
      <MyComponents />
    </ThemeContext.Provider>
  );
}

function SomeDeeplyNestedChild() {
  // 取决于其子组件在哪里被渲染
  const theme = useContext(ThemeContext);
  // ...
}

когдаSomeDeeplyNestedChildПри рендеринге,useContext(ThemeContext)будет искать ближайшее дерево в<ThemeContext.Provider>, и используйте егоvalue.

(На самом деле React поддерживает стек контекста во время рендеринга.)

если нетThemeContext.Providerсуществует,useContext(ThemeContext)Результат вызова будет названcreateContext()заменено переданным значением по умолчанию. В приведенном выше примере это значение равно'light'.

побочный эффект

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

В React все это можно сделать, объявив эффекты:

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

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

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

Эффекты могут нуждаться в очистке, как в случае подписки на источник данных. Чтобы очистить его после подписки, эффект может вернуть функцию:

  useEffect(() => {
    DataSource.addSubscription(handleChange);
    return () => DataSource.removeSubscription(handleChange);
  });

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);

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

Например, следующий код содержит ошибки:

  useEffect(() => {
    DataSource.addSubscription(handleChange);
    return () => DataSource.removeSubscription(handleChange);
  }, []);

он содержит ошибки, потому что[]Представляет «не выполнять этот эффект повторно».handleChangeопределяется снаружи.handleChangeВозможно, обратитесь к любому реквизиту или состоянию:

  function handleChange() {
    console.log(count);
  }

Если мы не сделаем эффект снова позвонить,handleChangeвсегда будет первой версией рендеринга, аcountвсегда будет0.

Чтобы исправить это, убедитесь, что вы объявили определенный массив зависимостей, который содержитвсеВещи, которые можно изменить, даже функции:

  useEffect(() => {
    DataSource.addSubscription(handleChange);
    return () => DataSource.removeSubscription(handleChange);
  }, [handleChange]);

в зависимости от вашего кода, после каждого рендераhandleChangeбудет отличаться, поэтому вы все равно можете увидеть ненужные повторные подписки.useCallbackможет помочь вам решить эту проблему. Кроме того, вы можете просто позволить ему повторно подписаться. например в браузереaddEventListenerAPI работает очень быстро, но отказ от его использования в компоненте может вызвать больше проблем, чем его реальная ценность.

(ты сможешьРеагировать на документациюСредняя школа Узнать больше оuseEffectи знание других хуков. )

пользовательский крючок

из-заuseStateа такжеuseEffectявляется вызовом функции, поэтому мы можем скомпоновать его в наши собственные хуки:

function MyResponsiveComponent() {
  const width = useWindowWidth(); // 我们自己的 Hook
  return (
    <p>Window width is {width}</p>
  );
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });
  return width;
}

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

(ты сможешьРеагировать на документациюУзнайте больше о создании собственных хуков в . )

статический порядок использования

ты можешь поставитьuseStateПодумайте о синтаксисе, определяющем «переменные состояния React». На самом деле это не синтаксис, и, конечно же, мы все еще пишем приложения на JavaScript. Но мы рассматриваем React как среду выполнения, потому что React использует JavaScript для описания всего дерева пользовательского интерфейса, а его функции, как правило, ближе к уровню языка.

Предположениеuseявляется синтаксисом, и имеет смысл использовать его на верхнем уровне функции компонента:

// 😉 注意:并不是真的语法
component Example(props) {
  const [count, setCount] = use State(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Что это означает, когда он помещается в условный оператор или вне компонента?

// 😉 注意:并不是真的语法

// 它是谁的...局部状态?
const [count, setCount] = use State(0);

component Example() {
  if (condition) {
    // 要是 condition 是 false 时会发生什么呢?
    const [count, setCount] = use State(0);
  }

  function handleClick() {
    // 要是离开了组件函数会发生什么?
    // 这和一般的变量又有什么区别呢?
    const [count, setCount] = use State(0);
  }

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

// 😉 注意:并不是真的语法
component Example(props) {
  // 只在这里有效
  const [count, setCount] = use State(0);

  if (condition) {
    // 这会是一个语法错误
    const [count, setCount] = use State(0);
  }

с участиемimportТо же самое верно для объявлений, которые полезны только на верхнем уровне модуля.

Конечно,useНе совсем синтаксис.(Это не дает много преимуществ и создает много трений.)

Однако в React действительно желательно, чтобы все вызовы хуков происходили только в верхней части компонента, а не в условном выражении. Эти крючкиправиловозможноlinter pluginуказано. Существует много жарких споров по поводу этого выбора дизайна, но на практике я не вижу в этом ничего запутанного. Я также писал о том, почему альтернативы, обычно предлагаемыене работаетстатья.

Внутренняя реализация хуков на самом делесвязанный список. когда ты звонишьuseState, мы перемещаем указатель на следующий элемент. Когда мы выходим из компонентакадр "дерево вызовов", результирующий список кэшируется до начала следующего рендеринга.

эта статьяКраткое введение в то, как хуки работают внутри. Массивы, возможно, являются лучшей моделью для объяснения их принципов, чем связанные списки:

// 伪代码
let hooks, i;
function useState() {
  i++;
  if (hooks[i]) {
    // 再次渲染时
    return hooks[i];
  }
  // 第一次渲染
  hooks.push(...);
}

// 准备渲染
i = -1;
hooks = fiber.hooks || [];
// 调用组件
YourComponent();
// 缓存 Hooks 的状态
fiber.hooks = hooks;

(Если вам это интересно, настоящий код находится вздесь. )

Это примерно каждыйuseState()Способ, как получить правильное состояние. как мыДоКак вы знаете, «сопоставление» не является чем-то новым для React — по той же причине согласование зависит от того, совпадают ли элементы до и после рендеринга.

знание не упомянуто

Мы затронули почти все важные аспекты среды выполнения React. Если вы дочитали эту статью до конца, значит, вы уже знаете React лучше, чем 90% разработчиков! В этом нет ничего плохого!

Конечно, есть места, которые я не упомянул, в основном потому, что мы о них мало что знаем. React на данный момент не имеет очень хорошей поддержки многопроходного рендеринга, то есть когда для рендеринга родительского компонента требуется информация от дочернего компонента.API обработки ошибокТакже пока нет версии для Hooks. Эти две проблемы можно решить вместе. Параллельный режим на данный момент не кажется стабильным, и есть много интересных вопросов о том, как Suspense вписывается в текущую версию. Может быть, я вернусь к ним, когда они закончат, и Suspense готов сравнитьlazy loadingможет больше.

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

Если вы помешаны на UI-библиотеках, я надеюсь, что эта статья будет вам интересна и прольет свет на то, как работает React. Или, может быть, вы чувствуете, что React слишком сложен для того, чтобы вы могли в нем разобраться. В любом случае, я хотел бы услышать от вас на Twitter! Спасибо за прочтение.