web-dev-qa-db-fra.com

Comment les crochets React déterminent-ils le composant auquel ils sont destinés?

J'ai remarqué que lorsque j'utilisais des hooks React, le changement d'état d'un composant enfant ne restitue pas un composant parent sans changement d'état. Ceci est vu par ce sandbox de code: https://codesandbox.io/s/kmx6nqr4o

En raison de l'absence de passage du composant au hook en tant qu'argument ou en tant que contexte de liaison, j'avais pensé à tort que les hooks de réaction/les changements d'état déclenchaient simplement un rendu d'application complet, comme le fonctionnement du mithril et ce que React Principes de conception indique:

React parcourt l'arborescence de manière récursive et appelle les fonctions de rendu de l'ensemble de l'arborescence mise à jour pendant un seul tick.

Au lieu de cela, il semble que les hooks réactifs sachent à quel composant ils sont associés et, par conséquent, le moteur de rendu ne sait que mettre à jour ce composant unique et n'appeler jamais render sur quoi que ce soit d'autre, contrairement à ce que le document React's Design Principles a dit ci-dessus. .

  1. Comment se fait l'association entre le crochet et le composant?

  2. Comment cette association fait-elle pour que React sache appeler uniquement render sur les composants dont l'état a changé, et non sur ceux sans? (dans le sandbox de code, malgré le changement d'état de l'enfant, le render de l'élément parent n'est jamais appelé)

  3. Comment cette association fonctionne-t-elle quand vous résumez l'utilisation de useState et setState dans des fonctions de hook personnalisées? (comme le fait le sandbox de code avec le crochet setInterval)

Il semble que les réponses se trouvent quelque part avec cette piste resolverDispatcher , ReactCurrentOwner , react-reconciler .

11
balupton

Tout d'abord, si vous recherchez une explication conceptuelle du fonctionnement des crochets et de la façon dont ils savent à quelle instance de composant ils sont liés, consultez les éléments suivants:

Le but de cette question (si je comprends bien l'intention de la question) est d'approfondir les détails d'implémentation réels de la façon dont React sait quelle instance de composant rendre à nouveau lorsque l'état change via un setter retourné par le crochet useState. Parce que cela va plonger dans les détails de l'implémentation de React, il est certain qu'elle deviendra progressivement moins précise à mesure que l'implémentation de React évoluera avec le temps. Lorsque je cite des parties du code React, je supprimerai les lignes qui me semblent obscurcir les aspects les plus pertinents pour répondre à cette question.

La première étape pour comprendre comment cela fonctionne est de trouver le code approprié dans React. Je me concentrerai sur trois points principaux:

  • le code qui exécute la logique de rendu pour une instance de composant (c'est-à-dire pour un composant de fonction, le code qui exécute la fonction du composant)
  • le code useState
  • le code déclenché en appelant le setter retourné par useState

Partie 1 Comment React connaît-elle l'instance de composant qui a appelé useState?

Une façon de trouver le code React qui exécute la logique de rendu consiste à renvoyer une erreur à partir de la fonction de rendu. La modification suivante du CodeSandbox de la question permet de déclencher facilement cette erreur:

Edit React hooks parent vs child state

Cela nous fournit la trace de pile suivante:

Uncaught Error: Error in child render
    at Child (index.js? [sm]:24)
    at renderWithHooks (react-dom.development.js:15108)
    at updateFunctionComponent (react-dom.development.js:16925)
    at beginWork$1 (react-dom.development.js:18498)
    at HTMLUnknownElement.callCallback (react-dom.development.js:347)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:397)
    at invokeGuardedCallback (react-dom.development.js:454)
    at beginWork$$1 (react-dom.development.js:23217)
    at performUnitOfWork (react-dom.development.js:22208)
    at workLoopSync (react-dom.development.js:22185)
    at renderRoot (react-dom.development.js:21878)
    at runRootCallback (react-dom.development.js:21554)
    at eval (react-dom.development.js:11353)
    at unstable_runWithPriority (scheduler.development.js:643)
    at runWithPriority$2 (react-dom.development.js:11305)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11349)
    at flushSyncCallbackQueue (react-dom.development.js:11338)
    at discreteUpdates$1 (react-dom.development.js:21677)
    at discreteUpdates (react-dom.development.js:2359)
    at dispatchDiscreteEvent (react-dom.development.js:5979)

Je vais donc d'abord me concentrer sur renderWithHooks. Cela réside dans ReactFiberHooks . Si vous souhaitez explorer davantage le chemin vers ce point, les points clés situés plus haut dans la trace de la pile sont les fonctions beginWork et pdateFunctionComponent qui se trouvent toutes deux dans ReactFiberBeginWork.js.

Voici le code le plus pertinent:

    currentlyRenderingFiber = workInProgress;
    nextCurrentHook = current !== null ? current.memoizedState : null;
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
    let children = Component(props, refOrContext);
    currentlyRenderingFiber = null;

currentlyRenderingFiber représente l'instance de composant en cours de rendu. C'est ainsi que React sait à quelle instance de composant un appel useState est lié. Peu importe la profondeur dans les hooks personnalisés que vous appelez useState, cela se produira toujours dans le rendu de votre composant (se produisant dans cette ligne: let children = Component(props, refOrContext);), donc React saura toujours qu'il est lié à l'ensemble currentlyRenderingFiber avant le rendu.

Après avoir défini currentlyRenderingFiber, il définit également le répartiteur actuel. Notez que le répartiteur est différent pour le montage initial d'un composant (HooksDispatcherOnMount) par rapport à un nouveau rendu du composant (HooksDispatcherOnUpdate). Nous reviendrons sur cet aspect dans la partie 2.

Partie 2 Que se passe-t-il dans useState?

Dans ReactHooks nous pouvons trouver ce qui suit:

    export function useState<S>(initialState: (() => S) | S) {
      const dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }

Cela nous amènera à la fonction useState dans ReactFiberHooks . Ceci est mappé différemment pour le montage initial d'un composant par rapport à une mise à jour (c'est-à-dire un nouveau rendu).

const HooksDispatcherOnMount: Dispatcher = {
  useReducer: mountReducer,
  useState: mountState,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  useReducer: updateReducer,
  useState: updateState,
};

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

La partie importante à noter dans le code mountState ci-dessus est la variable dispatch. Cette variable est le setter de votre état et est renvoyée par mountState à la fin: return [hook.memoizedState, dispatch];. dispatch n'est que la fonction dispatchAction (également dans ReactFiberHooks.js) avec certains arguments liés, y compris currentlyRenderingFiber et queue. Nous verrons comment ceux-ci entrent en jeu dans la partie 3, mais notez que queue.dispatch Pointe vers cette même fonction dispatch.

useState délègue à updateReducer (également dans ReactFiberHooks ) pour le cas de mise à jour (re-rendu). Je laisse intentionnellement de côté de nombreux détails de updateReducer ci-dessous, sauf pour voir comment il gère le retour du même setter que l'appel initial.

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      return [hook.memoizedState, dispatch];
    }

Vous pouvez voir ci-dessus que queue.dispatch Est utilisé pour renvoyer le même setter lors du re-rendu.

Partie 3 Que se passe-t-il lorsque vous appelez le setter retourné par useState?

Voici la signature de dispatchAction :

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A)

Votre nouvelle valeur d'état sera le action. fiber et work queue seront transmis automatiquement en raison de l'appel de bind dans mountState. fiber (le même objet enregistré précédemment sous currentlyRenderingFiber qui représente l'instance de composant) pointera vers la même instance de composant qui a appelé useState permettant à React de mettre en file d'attente le re-rendu de ce composant spécifique lorsque vous lui donnez une nouvelle valeur d'état.

Quelques ressources supplémentaires pour comprendre le React Fibre Reconciler et ce que sont les fibres:

19
Ryan Cogswell