反応フックを使用しているときに、子コンポーネントの状態変化が、状態変化のない親コンポーネントを再レンダリングしないことに気付きました。これは次のコードサンドボックスで確認できます: https://codesandbox.io/s/kmx6nqr4o
引数として、またはバインドコンテキストとしてコンポーネントをフックに渡さないため、フック/状態の変化に反応すると、ミスリルの仕組みやReactの動作など、アプリケーション全体のレンダリングがトリガーされると誤って考えていました 設計原則 の状態:
Reactはツリーを再帰的にたどり、1ティックの間に更新されたツリー全体のレンダリング関数を呼び出します。
代わりに、reactフックはそれらが関連付けられているコンポーネントを認識しているようです。したがって、レンダリングエンジンはその単一のコンポーネントのみを更新することを認識し、他のものに対してはrender
を呼び出さないため、上記のReactの設計原則のドキュメントに反します。 。
フックとコンポーネント間の関連付けはどのように行われますか?
この関連付けにより、reactは状態が変化したコンポーネントではrender
のみを呼び出し、それがないコンポーネントでは呼び出さないようにするにはどうすればよいでしょうか。 (コードサンドボックスでは、子の状態が変化しても、親要素のrender
が呼び出されることはありません)
UseStateとsetStateの使用をカスタムフック関数に抽象化しても、この関連付けはどのように機能しますか? (コードサンドボックスがsetInterval
フックで行うように)
答えはこの証跡のどこかにあるようです resolveDispatcher 、 ReactCurrentOwner 、 react-reconciler 。
まず、フックがどのように機能するか、どのコンポーネントインスタンスに関連付けられているかをフックが概念的に説明している場合は、以下を参照してください。
この質問の目的(質問の意図を正しく理解している場合)は、によって返されるセッターを介して状態が変化したときにReactがどのコンポーネントインスタンスを再レンダリングするかを知る実際の実装の詳細をさらに詳しく知ることですuseState
フック。これはReactの実装の詳細を掘り下げるため、Reactの実装が時間の経過とともに進化するにつれて、徐々に精度が低下することは確実です。 Reactコードの一部を引用するとき、この質問に答えるための最も関連性のある側面を難読化すると感じる行を削除します。
これがどのように機能するかを理解する最初のステップは、React内で関連するコードを見つけることです。私は3つの主要なポイントに焦点を当てます:
useState
コードuseState
によって返されるセッターを呼び出すことによってトリガーされるコードパート1Reactは、useState
を呼び出したコンポーネントインスタンスをどのように認識しますか?
レンダリングロジックを実行するReactコードを見つける1つの方法は、render関数からエラーをスローすることです。質問のCodeSandboxの次の変更は、そのエラーをトリガーする簡単な方法を提供します。
これにより、次のスタックトレースが提供されます。
_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);
}
_
ReactFiberHooks のuseState
関数が表示されます。これは、コンポーネントの初期マウントと更新(つまり、再レンダリング)では異なる方法でマッピングされます。
_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
は、dispatchAction
とcurrentlyRenderingFiber
を含むいくつかの引数がバインドされたqueue
関数(ReactFiberHooks.jsにも含まれる)です。これらが第3部でどのように機能するかを見ていきますが、_queue.dispatch
_がこの同じdispatch
関数を指していることに注意してください。
更新(再レンダリング)の場合、useState
はupdateReducer
にデリゲートします(これも 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とファイバーとは何かを理解するための追加リソース: