Reactのアーキテクチャを知る useState編4
こんにちは、noyanです。React内部実装アドベントカレンダーの5日目です。昨日まで3回にわけてマウント時のuseState処理を追ってきました。ですが、詳細に関数の処理を追うことに注力したため、実際に関数コンポーネントと関数の知識が結びついていない感じがします。
以上を背景に、この記事では関数コンポーネントがマウントされてから値が更新されるまでの流れを概観し、useStateの全体像をつかみます。useStateのメンタルモデルを作成して、hookの挙動を想定できるようになることがgoalです。
関数コンポーネントが更新されるまで
次のような関数コンポーネントがあるとします。
const Counter = () => { const [count, setCount] = useState(0); if(count === 0) setCount(1); return (<div>{count}</div>) }
この関数が<div>1</div>
となるまでの処理を追っていきましょう。これには主にrenderWithHooksの処理をおえば良いです。
1. useStateのdispatcherを決定
renderWithHooksが呼ばれると、ReactはuseStateに紐付くdispatcherを決定します。onMountではmountState関数が呼び出されると、hookを作成し[hook.memoizedState, dispatch]
を返します。
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRanderLane ): any { // Attach Dispatcher ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // Render components as long as it has any updates let children = Component(props, secondArg); if (didScheduleRenderPhaseUpdateDuringThisPass) { do { didScheduleRenderPhaseUpdateDuringThisPass = false; ReactCurrentDispatcher.current = HooksDispatcherOnRerender; children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); } return children; }
2. コンポーネントのレンダリング - mountState編
次にコンポーネント関数を実行します。
let children = Component(props, context);
記事冒頭のCounterコンポーネントの場合で処理を追ってみましょう。
const Counter = (props, context) => { // 引数には ({}, undefined)が入ることを期待している。 const [count, setCount] = useState(0); if(count === 0) setCount(1); return (<div>{count}</div>) } let children = Counter(props, context)
(第2引数のcontextは非推奨のLegacy Context APIで、現在標準のcontextの表記法とは異なります。)
見通しを良くするため、実行される関数に置き換えます。
- useStateはマウント時mountStateに
- setStateはdispatchに
const Counter = (props, context) => { // 引数には ({}, undefined)が入ることを期待している。 const [hook.memoizedState, dispatch] = mountState(0); // ↑ === 0 if(count === 0) dispatch(1) ); return (<div>{hook.memoizedState}</div>); } let children = Counter(props, context)
mountState
Counter関数で最初に呼び出されるmountState関数は[0, setCount]
を返します。この際、Reactは内部でhook.memoizedStateにmountStateの呼び出し値を記録し、次回呼ばれた際に返す値とします。
dispatch
dispatch関数はバインドされたdispatchSetStateです。dispatchSetState関数は更新キューをつくり、hook.queue.pendingに循環リストのかたちで付与します。
Counter関数でもsetState関数(=dispatchSetState)が呼ばれるので、dispatch(1)
によりhook.queue.pendingに更新キューが作成されます。
(この部分でわからない箇所がある。dispatchSetStateはマウント時の関数が使い回されるため、この関数はマウント時のfiberをずっと参照している。するとマウント時以外はrerender更新は行われないことになり、これは即座の反映が行われないことを意味する。この理解は正しいのだろうか。)
3. コンポーネントのレンダリング - rerenderState編
実はdispatchSetStateを実行するとdidScheduleRenderPhaseUpdateDuringThisPass
がtrue
になるので、値がfalse
になるまでレンダリングを続けます。アプリケーションコードとしては、setStateが呼ばれなくなるまで実行することを意味します。
function renderWithHooks(){ if (didScheduleRenderPhaseUpdateDuringThisPass) { do { didScheduleRenderPhaseUpdateDuringThisPass = false; ReactCurrentDispatcher.current = HooksDispatcherOnRerender; children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); } return children; } const HooksDispatcherOnRerender: Dispatcher = { useMemo: updateMemo, useReducer: rerenderReducer, useRef: updateRef, useState: rerenderState, }
do while 構文内のDispatcher.useStateは、処理されていないqueueを順番に処理します。これが前回読んだrerenderStateの仕事です。結果的にhooksの値が更新されます。
const [count, setCount] = useState(0); ↑ === 1
やっと更新されました。
コンポーネントが最後まで実行され、childrenに値が代入されます。
children = <div>0</div>
さて、count === 1のときsetStateは実行されなかったため、didScheduleRenderPhaseUpdateDuringThisPass
はfalseのままになり、do while文を抜けます。
最終的に更新された_jsxが最終的にReactElementを呼び出して、renderWithHooksの呼び出しが終了します。この後どうなるかは自分もわかっていないですし、急に知らなければいけない知識が多くなってしまうので今度にさせてください。
おわりに
本当はdo while文の後にcontextの最適化処理があるのですが、これは今回関係ないので省略しました。この話が今月のReact confで発表されると思われるので、楽しみにしています。
次回はすこし箸休めとしてdev-toolかjsxの変換の話をしようと思います。