燒腦預(yù)警,這波心智負擔(dān)有點重,深度探討 useState 的實現(xiàn)原理
在前面的一篇文章中,我們介紹了 Fiber 的詳細屬性所代表的含義。在函數(shù)式組件中,其中與 hook 相關(guān)的屬性為 memoizedState。
Fiber = {
memoizedState: Hook
}
Fiber.memoizedState 是一個鏈表的起點,該鏈表的節(jié)點信息為。
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
}
useState 調(diào)用分為兩個階段,一個是初始化階段,一個是更新階段。當我們在 beginWork 中調(diào)用 renderWithHooks 時,通過判斷 Fiber.memozedState 是否有值來分辨當前執(zhí)行屬于初始階段還是更新階段。
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
在 react 模塊中,我們可以看到 useState 的源碼非常簡單。
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
這里的 dispatcher,其實就是我們在 react-reconciler 中判斷好的 ReactCurrentDispatcher.currenthook 的初始化方法掛載在 HooksDispatcherOnMount 上。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
hook 的更新方法掛載在 HooksDispatcherOnUpdate 上。
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
因此,在初始化時,useState 調(diào)用的是 mountState,在更新時,useState 調(diào)用的是 updateState
一、mountState
mountState 的源碼如下:
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: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
理解這個源碼的關(guān)鍵在第一行代碼。
const hook = mountWorkInProgressHook();
react 在 ReactFiberHooks.new.js 模塊全局中創(chuàng)建了如下三個變量。
let currentlyRenderingFiber: Fiber = (null: any);
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
currentlyRenderingFiber 表示當前正在 render 中的 Fiber 節(jié)點。currentHook 表示當前 Fiber 的鏈表。
workInProgressHook 表示當前正在構(gòu)建中的新鏈表。
mountWorkInProgressHook 方法會創(chuàng)建當前這個 mountState 執(zhí)行所產(chǎn)生的 hook 鏈表節(jié)點。
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 作為第一個節(jié)點
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 添加到鏈表的下一個節(jié)點
workInProgressHook = workInProgressHook.next = hook;
}
// 返回當前節(jié)點
return workInProgressHook;
}
hook 節(jié)點的 queue 表示一個新的鏈表結(jié)構(gòu),用于存儲針對同一個 state 的多次 update 操作。,.pending 指向下一個 update 鏈表節(jié)點。此時因為是初始化操作,因此值為 null,此時我們會先創(chuàng)建一個 queue。
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
此時,dispatch 還沒有賦值。在接下來我們調(diào)用了 dispatchSetState,我們待會兒來詳細介紹這個方法,他會幫助 queue.pending 完善鏈表結(jié)構(gòu)或者進入調(diào)度階段,并返回了當前 hook 需要的 dispatch 方法。
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
最后將初始化之后的緩存值和操作方法通過數(shù)組的方式返回。
return [hook.memoizedState, dispatch];
二、updateState
更新時,將會調(diào)用 updateState 方法,他的代碼非常簡單,就是直接調(diào)用了一下 updateReducer。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
這里的需要注意的是有一個模塊中的全局方法 basicStateReducer,該方法執(zhí)行會結(jié)合傳入的 action 返回最新的 state 值。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
代碼中區(qū)分的情況是 useState 與 useReducer 的不同。useState 傳入的是值,而 useReducer 傳入的是函數(shù)
三、updateReducer
updateReducer 的代碼量稍微多了一些,不過他的主要邏輯是計算出最新的 state 值。
當我們使用 setState 多次調(diào)用 dispatch 之后, 在 Hook 節(jié)點的 hook.queue 上會保存一個循環(huán)鏈表用于存儲上一次的每次調(diào)用傳入的 state 值,updateReducer 的主要邏輯就是遍歷該循環(huán)鏈表,并計算出最新值。
此時首先會將 queue.pending 的鏈表賦值給 hook.baseQueue,然后置空 queue.pending。
const pendingQueue = queue.pending;
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
然后通過 while 循環(huán)遍歷 hook.baseQueue 通過 reducer 計算出最新的 state 值。
// 簡化版代碼
const first = baseQueue.next;
if (first !== null) {
let newState = current.baseState;
let update = first;
do {
// 執(zhí)行每一次更新,去更新狀態(tài)
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
最后再返回。
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
四、dispatchSetState
當我們調(diào)用 setState 時,最終調(diào)用的是 dispatchSetState 方法。
setLoading -> dispatch -> dispatchSetState
該方法有兩個邏輯,一個是同步調(diào)用,一個是并發(fā)模式下的異步調(diào)用。
同步調(diào)用時,主要的目的在于創(chuàng)建 hook.queue.pending 指向的環(huán)形鏈表。
首先我們要創(chuàng)建一個鏈表節(jié)點,該節(jié)點我們稱之為 update。
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
然后會判斷是否在 render 的時候調(diào)用了該方法。
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
isRenderPhaseUpdate 用于判斷當前是否是在 render 時調(diào)用,他的邏輯也非常簡單。
function isRenderPhaseUpdate(fiber: Fiber) {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}
這里需要重點關(guān)注是 enqueueRenderPhaseUpdate 是如何創(chuàng)建環(huán)形鏈表的。他的代碼如下:
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
我們用圖示來表達一下這個邏輯,光看代碼可能理解起來比較困難。
當只有一個 update 節(jié)點時。
新增一個:
再新增一個:
在后續(xù)的邏輯中,會面臨的一種情況是當渲染正在發(fā)生時,收到了來自并發(fā)事件的更新,我們需要等待直到當前渲染結(jié)束或中斷再將其加入到 Fiber/Hook 隊列。因此React 需要一個數(shù)組來存儲這些更新,代碼邏輯如下:
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
在這個基礎(chǔ)之上,React 就有機會處理那些不會立即導(dǎo)致重新渲染的更新進入隊列。如果后續(xù)有更高優(yōu)先級的更新出現(xiàn),將會重新對其進行排序。
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
): void {
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
}
dispatchSetState 的邏輯中,符合條件就會執(zhí)行該函數(shù)。
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
很顯然,這就是并發(fā)更新的邏輯,代碼會最終調(diào)用 scheduleUpdateOnFiber,該方法是由 react-reconciler 提供,他后續(xù)會將任務(wù)帶入到 scheduler 中調(diào)度。
// 與 enqueueConcurrentHookUpdateAndEagerlyBailout 方法邏輯
// 但會返回 root 節(jié)點
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
五、總結(jié)與思考
這就是 useState 的實現(xiàn)原理。其中包含了大量的邏輯操作,可能跟我們在使用時所想的那樣有點不太一樣。這里大量借助了閉包和鏈表結(jié)構(gòu)來完成整個構(gòu)想。
這個邏輯里面也會有大量的探討存在于大廠面試的過程中。例如
- 為什么不能把 hook 的寫法放到 if 判斷中去。
- setState 的合并操作是如何做到的。
- hook 鏈表和 queue.pending 的環(huán)狀鏈表都應(yīng)該如何理解?
- setState 之后,為什么無法直接拿到最新值,徹底消化了之后這些問題都能很好的得到解答。