文章的 React バージョンは v17.02 の concurrent モードに基づいています。
hooks のリンクリストは、すべての hook を説明する前に避けて通れないトピックです。本記事では、デバッグの観点から hooks のリンクリストの構造と機能を分析します。
コンポーネントのマウント時のリンクリストロジック#
まず、このコードを録画してみましょう。
import { useLayoutEffect, useEffect, useState } from "react";
function UseEffectAnduseLayoutEffect() {
const [text, setText] = useState('wawawa');
useEffect(() => {
console.log('useEffect create 1');
// setText('useEffect create 1');
return () => {
console.log('useEffect destroy 1');
// setText('useEffect destroy 1');
}
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect create 1');
// setText('useLayoutEffect create 1');
return () => {
console.log('useLayoutEffect destroy 1');
// setText('useLayoutEffect destroy 1');
}
}, []);
return (
<div onClick={() => { setText('hahaha') }}>{ text }</div>
)
}
export default UseEffectAnduseLayoutEffect;
結果から、renderWithHooks という関数に注目することができます。この関数内で UseEffectAnduseLayoutEffect が実行され、関数コンポーネントが関数として呼び出されること自体がレンダリングの一環です。さらに、この関数内でコード中の hook も呼び出されています。
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
...
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg); // ここではUseEffectAnduseLayoutEffectを呼び出すシーンに注目します
...
return children;
}
React 内で current の有無によってマウントか更新かを判断するのは一般的な操作です。現在記録しているのは初回マウント時の操作であるため、この時 ReactCurrentDispatcher.current は HooksDispatcherOnMount です。なぜマウント / 更新を区別する必要があるのかについては、後で紹介します。
次に、UseEffectAnduseLayoutEffect 内で呼び出される useState と useEffect に注目します。hooks のリンクリストを構築する関連ロジックに入ります。
UseEffectAnduseLayoutEffect の例では、最初に呼び出されるのは useState であり、そのブレークポイントをデバッグすると mountState に入ります。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
しかし、現在のコードでは mountWorkInProgressHook に注目する必要があります。これは hook オブジェクトを構築するためのものです。
hooks 関数が呼び出されるたびに hook オブジェクトが作成され、その構造は以下の通りです。
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
}; // hookオブジェクトの構造はhooksリンクリストの最小単位です
if (workInProgressHook === null) {
// これはリストの最初のhookです
/**
* ここで、fiber内のmemoizedStateはhooksリンクリストを保存するために使用されます
*/
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// リストの末尾に追加します
/**
* 後続の他のhookを呼び出すと、前のhookオブジェクトの後ろに直接接続されます
*/
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
上記のコードでは、workInProgressHook というポインタに注意してください。これは現在のコンポーネント内でどの hook 関数が呼び出されたかを反映しています。
コード内の useState、useEffect、useLayout がすべて実行された後、完全な hooks リンクリストを取得します。
この時、workInProgressHook が最後の hook オブジェクトを指していることも確認できます。
コンポーネント更新時のリンクリストロジック#
クリックイベントをトリガーする記録を録画し、以下の結果を得ました。
この時、ReactCurrentDispatcher.current は HooksDispatcherOnUpdate です。したがって、再度 UseEffectAnduseLayoutEffect に入ると、呼び出される hooks はマウント時とは異なります。この時、再度 useState にブレークポイントを設定すると、updateState 内の updateWorkInProgressHook に入ります。
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// 注釈に基づいて、current hookリストは現在のfiberに属するリストであることがわかります
// 更新操作時には、currentツリーがすでに存在する必要があります
// currentHookは関数コンポーネントの呼び出しが完了した時にnullに設定されるため、再レンダリングの開始を確認するために使用できます
if (currentHook === null) {
/**
* 注釈に基づいて、作業中のfiberです。作業中のhookと区別するために異なる名前を付けました。
* currentlyRenderingFiberはWIPのfiberです
* したがって、そのalternateは対応するcurrent fiberノードを指します
*/
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// currentノードが存在する場合、nextCurrentHookをcurrent.memoizedState(hooksリンクリスト)に指します
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// ポインタを次に移動します
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// すでに作業中のものがあります。再利用します。
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 現在のhookからクローンします。
invariant(
nextCurrentHook !== null,
'前のレンダリング中にレンダリングされたhookの数が多すぎます。',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// これはリストの最初のhookです。
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// リストの末尾に追加します。
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
コードは冗長で、ポインタが飛び回りますが、理解する最良の方法は自分でデバッグすることです。ここで最も重要な情報は、更新時に current ノード上の hooks リンクリストを使用して新しい hooks リンクリストを生成することです。状態の更新方法については、本記事では紹介しません。
まとめ#
- 関数コンポーネントを呼び出すたびに、対応する fiber の memoizedState にマウントされた hooks リンクリストが生成されます。