Как триггеры React.useState перерисовывают?

import { useState } from 'react';

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

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

В приведенном выше примере всякий раз, когда setCount(count + 1) вызывается повторный рендеринг. Мне любопытно узнать поток.

Я пытался заглянуть в исходный код. Я не мог найти ссылку на useState или другие хуки на github.com/facebook/react.

Я установил react@next с помощью npm i react@next и нашел следующее в node_modules/react/cjs/react.development.js

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

На пути к dispatcher.useState() Я мог только найти следующее...

function resolveDispatcher() {
  var dispatcher = ReactCurrentOwner.currentDispatcher;
  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
  return dispatcher;
}
var ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null,
  currentDispatcher: null
};

Интересно, где я могу найти dispatcher.useState() реализации и узнать, как это вызывает повторную визуализацию, когда setStatesetCount вызывается.

Любой указатель будет полезен.

Спасибо!

1 ответ

Ключ к пониманию этого - следующий абзац из FAQ по хукам.

Как React связывает вызовы Hook с компонентами?

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

С каждым компонентом связан внутренний список "ячеек памяти". Это просто объекты JavaScript, куда мы можем поместить некоторые данные. Когда вы вызываете Hook, например useState(), он считывает текущую ячейку (или инициализирует ее во время первого рендеринга), а затем перемещает указатель на следующую. Вот так каждый из нескольких вызовов useState() получает независимое локальное состояние.

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

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

Пример начинается, когда компонент монтируется и useState() (определено в строке 1581) вызывается впервые.

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

Как вы заметили, это вызывает resolveDispatcher()(определено в строке 1546). Вdispatcherвнутренне относится к компоненту, который в настоящее время отображается. Внутри компонента вы можете (если вы осмелились уволиться) взглянуть на диспетчер, например, через

console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)

Если вы примените это в случае встречного примера, вы заметите, что dispatcher.useState()относится к коду react-dom. При первой установке компонентаuseState относится к определенному в строке 15986, который вызывает mountState(). При повторном рендеринге изменился диспетчер и функцияuseState() на линии 16077 срабатывает, которая вызывает updateState(). Оба метода,mountState() на линии 15352 и updateState() в строке 15371 верните count, setCount пара.

Отслеживание ReactCurrentDispatcherстановится довольно грязным. Однако самого факта его существования уже достаточно, чтобы понять, как происходит повторный рендеринг. Волшебство происходит за сценой. Как указано в FAQ, React отслеживает текущий визуализированный компонент. Это означает,useState() знает, к какому компоненту он прикреплен, как найти информацию о состоянии и как запустить повторный рендеринг.

Я также попытался понять логику useState в очень упрощенной и базовой манере, если мы просто рассмотрим его основные функции, исключая оптимизацию и асинхронное поведение, то мы обнаружили, что он в основном выполняет 4 общие функции:

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

Имея это в виду, я пришел к следующему фрагменту

const Demo = (function React() {
  let workInProgress = false;
  let context = null;

  const internalRendering = (callingContext) => {
    context = callingContext;
    context();
  };

  const intialRender = (component) => {
    context = component;
    workInProgress = true;
    context.state = [];
    context.TotalcallerId = -1; // to store the count of total number of useState within a component
    context.count = -1; // counter to keep track of useStates within component
    internalRendering(context);
    workInProgress = false;
    context.TotalcallerId = context.count;
    context = null;
  };

  const useState = (initState) => {
    if (!context) throw new Error("Can only be called inside function");

     // resetting the count so that it can maintain the order of useState being called

    context.count =
      context.count === context.TotalcallerId ? -1 : context.count; 

    let callId = ++context.count;

    // will only initialize the value of setState on initial render
    const setState =
      !workInProgress ||
      (() => {
        const instanceCallerId = callId;
        const memoizedContext = context;
        return (updatedState) => {
          memoizedContext.state[instanceCallerId].value = updatedState;
          internalRendering(memoizedContext);
        };
      })();

    context.state[callId] = context.state[callId] || {
      value: initState,
      setValue: setState,
    };

    return [context.state[callId].value, context.state[callId].setValue];
  };

  return { useState, intialRender };
})();

const { useState, intialRender } = Demo;

const Component = () => {
  const [count, setCount] = useState(1);
  const [greeting, setGreeting] = useState("hello");

  const changeCount = () => setCount(100);
  const changeGreeting = () => setGreeting("hi");

  setTimeout(() => {
    changeCount();
    changeGreeting();
  }, 5000);

  return console.log(`count ${count} name ${greeting}`);
};

const anotherComponent = () => {
  const [count, setCount] = useState(50);
  const [value, setValue] = useState("World");

  const changeCount = () => setCount(500);
  const changeValue = () => setValue("React");

  setTimeout(() => {
    changeCount();
    changeValue();
  }, 10000);

  return console.log(`count ${count} name ${value}`);
};
intialRender(Component);
intialRender(anotherComponent);

здесь useState и initialRender взяты из Demo. intialRender используется для первоначального вызова компонентов, он сначала инициализирует контекст, а затем в этом контексте устанавливает состояние как пустой массив (для каждого компонента есть несколько useState, поэтому нам нужен массив для его поддержки), а также нам нужен счетчик, чтобы сделать count для каждого useState и TotalCounter для хранения общего количества вызываемых useState для каждого компонента.

setState это метод на Component/PureComponent класс, поэтому он будет делать все, что реализовано в Component класс (включая вызов render метод).

setState выгружает обновление состояния в enqueueSetState поэтому тот факт, что это связано с этим, на самом деле является лишь следствием использования классов и расширения Component, Однажды вы понимаете, что обновление состояния на самом деле не обрабатывается самим компонентом и this это просто удобный способ доступа к функциональности обновления состояния, тогда useState отсутствие явной привязки к вашему компоненту имеет гораздо больше смысла.

FunctionComponent отличается. В прошлом они были чистыми, простыми. Но теперь у них есть свое государство. Легко забыть, что реагирование на использование createElement обертывает весь узел JSX, включая FunctionComponent.

function FunctionComponent(){
  return <div>123</div>;
}
const a=<FunctionComponent/>
//after babel transform
function FunctionComponent() {
  return React.createElement("div", null, "123");
}

var a = React.createElement(FunctionComponent, null);

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

Другие вопросы по тегам