В чем разница между компонентами Function и Classes?

React.js

В чем разница между функциональными компонентами React и классами React?

Ранее стандартным ответом было сказать, что классы предоставляют больше функций (например, состояние). имеютHooks, это не так.

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

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

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

Функциональный компонент фиксирует значение после рендеринга.

Давайте проанализируем, что это значит.


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


Рассмотрим этот компонент:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

Он показывает фиктивный сетевой запрос сsetTimeoutИ тогда кнопка появится во всплывающем окне подтверждения. Например, еслиprops.userдля'Dan', он появится через три секунды'Followed Dan',очень простой.

(Обратите внимание, что в приведенном выше примере, независимо от того, использую ли я стрелки или обычные функции,function handleClick()Определенно тот же эффект. )

Так что, если мы напишем это как класс? Это может выглядеть так после прямого перевода:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

Эти две части кода обычно считаются эквивалентными, и люди часто произвольно рефакторят эти шаблоны, не замечая, что они означают:

Spot the difference between two versions

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

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


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

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


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

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

Используйте две кнопки в этой последовательности операций:

  1. нажмитеодна из кнопок.
  2. через 3 секундыИзменятьВыберите запись.
  3. посмотриТекст, который всплывает.

Вы заметите особую разницу:

  • так какfunctionизProfilePage, нажмите «Подписаться на запись Дэна», а затем переключитесь на запись Софи, все еще всплывает'Followed Dan'.

  • так какclassизProfilePage, он появится'Followed Sophie':

Demonstration of the steps


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


Так почему же наш пример класса ведет себя именно так?

Давайте поближе познакомимся с классомshowMessageметод:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

Этот метод класса читаетthis.props.userPROPS не является переменным в React.но,this да, и изменилось.

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

Поэтому, если наш компонент повторно отображается в состоянии запроса,this.propsизменится.showMessageметод от "слишком новый"propsполучено вuser.

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

Однако планирование резервного чтенияthis.propsТайм-аут разорвет соединение. нашshowMessageОбратный вызов не «привязан» к какому-либо конкретному рендеру, поэтому он «теряет» правильные реквизиты и вместо этого читаетthisПрервите эту связь.


Можно сказать, что функциональный компонент не имеет этой проблемы.. Как мы собираемся решить эту проблему?

Мы хотим как-то «починить» правильный реквизитrenderи получить ихshowMessageСвязи между обратными вызовами. по этому путиpropsбудет потерян.

Один из способов — читать в начале событияthis.props, а затем явно передать их обработчику времени ожидания:

class ProfilePage extends React.Component {
  showMessage = (user) => {
    alert('Followed ' + user);
  };

  handleClick = () => {
    const {user} = this.props;
    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

такдостижимый. Однако такой подход делает код значительно более избыточным и подверженным ошибкам с течением времени. Что делать, если нам нужно более одного реквизита? Что, если нам тоже нужно получить состояние?еслиshowMessageвызываются другие методы, и этот метод читаетсяthis.props.somethingилиthis.state.something, у нас снова будет та же проблема. Так что мы должныthis.propsа такжеthis.stateпередается в качестве параметра каждому вызовуshowMessageМетоды.

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

Аналогично, положитьalertположить вhandleClickне может решить эту проблему. Мы хотим структурировать код таким образом, чтобы можно было разделить больше методов, а также считывать соответствующие реквизиты и состояние рендеринга, связанные с этим вызовом.Эта проблема даже не уникальна для реагирования - вы можете поставить данные во что угодноthisВоспроизведите его в библиотеке пользовательского интерфейса для изменяемых объектов..

Возможно, мы можем в конструктореbindметод?

class ProfilePage extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  showMessage() {
    alert('Followed ' + this.props.user);
  }

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

Нет, это ничего не исправляет. Помните, проблема в том, что мы читаем слишком поздноthis.propsНу - дело не в том, как мы используем грамматику!Однако эта проблема, если мы полагаемся полностью на закрытиях JavaScript, могут быть решены.

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

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

class ProfilePage extends React.Component {
  render() {
    // 捕获 props!
    const props = this.props;

    // 注意: 我们在 *render 里面*
    // 这不是 class 方法。
    const showMessage = () => {
      alert('Followed ' + props.user);
    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

У вас есть «захваченные» реквизиты во время рендеринга.:

Таким образом, любой код внутри него (включаяshowMessage) гарантированно увидят пропсы для этого конкретного рендера, и React больше не будет «трогать наш сыр».

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


пример вышеНичего страшного, но выглядит странно. Если вы определяете функцию в рендере вместо использования метода класса, что еще делает класс?

На самом деле мы можем упростить код, убрав класс «shell»:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

Как указано выше,propsВсе же поймал — React передает их как аргументы.В отличие отthis,propsСам объект не изменяется React.

Если деструктуризация во время определения функцииpropsЭто более очевидно:

function ProfilePage({ user }) {
  const showMessage = () => {
    alert('Followed ' + user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

Когда родительский компонент рендерится с разными реквизитамиProfilePage, React позвонит сноваProfilePageметод. Но программа событий, которую мы нажали на «принадлежит», имеет свою собственнуюuserзначение предыдущего рендера и прочитать егоshowMessageОбратный звонок, они все целы.

Вот почему, вэто демоВ функциональной версии переключение на Сунила после нажатия кнопки «Подписаться» во всплывающем окне «Софи».'Followed Sophie':

Этот ответ правильный.(Хотя вы также можетеСледуйте за Сунилом! )


Теперь мы понимаем самую большую разницу между функциями и классами в React:

Функциональный компонент фиксирует значение после рендеринга.

С хуками тот же принцип применяется к состоянию. Рассмотрим этот пример:

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

([Этоонлайн демо. ])

Хотя это не хорошая новость Application Ui, который реализует то же самое: если я отправлю конкретное сообщение, этот компонент не должен быть запутанным, какое сообщение для отправки. Эта функция компонентов消息Захватывает состояние и «принадлежит» возвращает рендеринг, вызванный событием щелчка браузера. так это消息устанавливается на значение на входе, когда я нажимаю «отправить».


Итак, мы знаем, что функции в React захватывают свойства и состояние по умолчанию.но если мынадеятьсяЧитайте последние реквизиты или состояние, они не относятся к конкретному рендеру, как это сделать? Если мы хотимчитать их в будущемКак сделать?

На уроках можно читатьthis.propsилиthis.state,потому чтоthisсам по себе изменчив, React изменит его. В функциональном компоненте вы также можете иметь изменяемое значение, совместно используемое всеми рендерингами компонента, называемое «ref»:

function MyComponent() {
  const ref = useRef(null);
  // 你可以读写 `ref.current`。
  // ...
}

Тем не менее, вы должны управлять им самостоятельно.

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

Даже визуально,this.sometingвыглядит какsomething.currentзеркальное отражение. Они представляют одно и то же понятие.

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

function MessageThread() {
  const [message, setMessage] = useState('');
  const latestMessage = useRef('');

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value;
  };

если мы прочитаемshowMessageизmessage, мы увидим сообщение, когда отправим кнопку. но когда мы читаемlatestMessage.current, мы получим последнее значение, даже если продолжаем печатать после нажатия кнопки отправки.

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

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

function MessageThread() {
  const [message, setMessage] = useState('');

  // 保持 track 是最新值
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

(Этоdemo. )

Мы в действиивНазначение для достижения значений ref меняется только при обновлении DOM. Это гарантирует, что наша мутация не нарушит такие вещи, какРазделение времени и саспенси другие функции, которые прерывают рендеринг.

refs редко используются таким образом,Захват свойств или состояния лучше по умолчанию. Однако при работе с такими вещами, как таймеры и подпискихитрые APIвремя очень удобное. Помните, что вы можете отслеживать любое такое значение — реквизиты, переменные состояния, весь объект реквизита или даже функцию.

Этот паттерн также можно использовать для оптимизации — например, вuseCallbackКогда индикатор часто меняется. но,использовать редукторОбычно одинлучшее решение. (Об этом будет написано в следующем блоге!)


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

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

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

Функции блокируют свои реквизиты и состояние — важно только то, что они собой представляют. Это не баг, а особенность функционального компонента. Например, функции не должны начинаться сuserEffectилиuseCallbackисключается из «массива зависимостей». (Обычно используемые соответствующие исправления, упомянутые выше, либоuseReducerилиuseRefрешение - мы скоро объясним, как выбирать между ними в документации)

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

Лучшее умственное правило, которое я нашел до сих пор с хуками, это «любое значение кода, кажется, меняется в любое время».

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

Функции React всегда фиксируют свои значения — и теперь мы знаем, почему.

Это совершенно разные покемоны.

перевести оригиналHow Are Function Components Different from Classes?(2019-03-03)