web-dev-qa-db-ja.com

反応フックは、それらが対象とするコンポーネントをどのように決定しますか?

反応フックを使用しているときに、子コンポーネントの状態変化が、状態変化のない親コンポーネントを再レンダリングしないことに気付きました。これは次のコードサンドボックスで確認できます: https://codesandbox.io/s/kmx6nqr4o

引数として、またはバインドコンテキストとしてコンポーネントをフックに渡さないため、フック/状態の変化に反応すると、ミスリルの仕組みやReactの動作など、アプリケーション全体のレンダリングがトリガーされると誤って考えていました 設計原則 の状態:

Reactはツリーを再帰的にたどり、1ティックの間に更新されたツリー全体のレンダリング関数を呼び出します。

代わりに、reactフックはそれらが関連付けられているコンポーネントを認識しているようです。したがって、レンダリングエンジンはその単一のコンポーネントのみを更新することを認識し、他のものに対してはrenderを呼び出さないため、上記のReactの設計原則のドキュメントに反します。 。

  1. フックとコンポーネント間の関連付けはどのように行われますか?

  2. この関連付けにより、reactは状態が変化したコンポーネントではrenderのみを呼び出し、それがないコンポーネントでは呼び出さないようにするにはどうすればよいでしょうか。 (コードサンドボックスでは、子の状態が変化しても、親要素のrenderが呼び出されることはありません)

  3. UseStateとsetStateの使用をカスタムフック関数に抽象化しても、この関連付けはどのように機能しますか? (コードサンドボックスがsetIntervalフックで行うように)

答えはこの証跡のどこかにあるようです resolveDispatcherReactCurrentOwnerreact-reconciler

16
balupton

まず、フックがどのように機能するか、どのコンポーネントインスタンスに関連付けられているかをフックが概念的に説明している場合は、以下を参照してください。

この質問の目的(質問の意図を正しく理解している場合)は、によって返されるセッターを介して状態が変化したときにReactがどのコンポーネントインスタンスを再レンダリングするかを知る実際の実装の詳細をさらに詳しく知ることですuseStateフック。これはReactの実装の詳細を掘り下げるため、Reactの実装が時間の経過とともに進化するにつれて、徐々に精度が低下することは確実です。 Reactコードの一部を引用するとき、この質問に答えるための最も関連性のある側面を難読化すると感じる行を削除します。

これがどのように機能するかを理解する最初のステップは、React内で関連するコードを見つけることです。私は3つの主要なポイントに焦点を当てます:

  • コンポーネントインスタンスのレンダリングロジックを実行するコード(つまり、関数コンポーネントの場合、コンポーネントの関数を実行するコード)
  • useStateコード
  • useStateによって返されるセッターを呼び出すことによってトリガーされるコード

パート1Reactは、useStateを呼び出したコンポーネントインスタンスをどのように認識しますか?

レンダリングロジックを実行するReactコードを見つける1つの方法は、render関数からエラーをスローすることです。質問のCodeSandboxの次の変更は、そのエラーをトリガーする簡単な方法を提供します。

Edit React hooks parent vs child state

これにより、次のスタックトレースが提供されます。

_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)
_

したがって、最初にrenderWithHooksに焦点を当てます。これは ReactFiberHooks 内にあります。このポイントへのパスをさらに探索したい場合、スタックトレースの上位にある重要なポイントは、ReactFiberBeginWork.jsにある beginWork および pdateFunctionComponent 関数です。

最も関連性の高いコードは次のとおりです。

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

currentlyRenderingFiberは、レンダリングされるコンポーネントインスタンスを表します。これは、ReactがuseState呼び出しがどのコンポーネントインスタンスに関連しているかを知る方法です。 useStateをどの程度深くカスタムフックに呼び出しても、コンポーネントのレンダリング内で発生します(この行で発生します:let children = Component(props, refOrContext);)。そのため、Reactは引き続き知っています。レンダリングの前にcurrentlyRenderingFiberセットに関連付けられていること。

currentlyRenderingFiberを設定すると、現在のディスパッチャーも設定されます。コンポーネント(HooksDispatcherOnMount)の初期マウントとコンポーネント(HooksDispatcherOnUpdate)の再レンダリングでは、ディスパッチャーが異なることに注意してください。パート2でこの側面に戻ります。

パート2useStateで何が起こりますか?

ReactHooks では、次のようになります。

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

ReactFiberHooksuseState関数が表示されます。これは、コンポーネントの初期マウントと更新(つまり、再レンダリング)では異なる方法でマッピングされます。

_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));
}
_

上記のmountStateコードで注目すべき重要な部分は、dispatch変数です。その変数は状態のセッターであり、最後にmountStateから返されます:_return [hook.memoizedState, dispatch];_。 dispatchは、dispatchActioncurrentlyRenderingFiberを含むいくつかの引数がバインドされたqueue関数(ReactFiberHooks.jsにも含まれる)です。これらが第3部でどのように機能するかを見ていきますが、_queue.dispatch_がこの同じdispatch関数を指していることに注意してください。

更新(再レンダリング)の場合、useStateupdateReducerにデリゲートします(これも ReactFiberHooks に含まれます)。以下のupdateReducerの詳細の多くは、最初の呼び出しと同じセッターを返す処理を確認することを除いて、意図的に省略しています。

_    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];
    }
_

上記を見ると、_queue.dispatch_が再レンダリング時に同じセッターを返すために使用されていることがわかります。

パート3useStateから返されたセッターを呼び出すとどうなりますか?

dispatchAction の署名は次のとおりです。

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

新しい状態値はactionになります。 fiber内のqueue呼び出しにより、bindおよび作業mountStateが自動的に渡されます。 fiber(コンポーネントインスタンスを表すcurrentlyRenderingFiberとして以前に保存された同じオブジェクト)は、useStateを呼び出した同じコンポーネントインスタンスをポイントし、Reactが特定のコンポーネントに新しい状態の値を与えると、そのコンポーネントの再レンダリングをキューに入れます。

React Fiber Reconcilerとファイバーとは何かを理解するための追加リソース:

19
Ryan Cogswell