當函數組件進入render階段時,會被renderWithHooks函數處理。函數組件作為壹個函數,它的渲染其實就是函數調用,而函數組件又會調用React提供的hooks函數。初始掛載和更新時,所用的hooks函數是不同的,比如初次掛載時調用的useEffect,和後續更新時調用的useEffect,雖然都是同壹個hook,但是因為在兩個不同的渲染過程中調用它們,所以本質上他們兩個是不壹樣的。這種不壹樣來源於函數組件要維護壹個hooks的鏈表,初次掛載時要創建鏈表,後續更新的時候要更新鏈表。
分屬於兩個過程的hook函數會在各自的過程中被賦值到ReactCurrentDispatcher
的current屬性上。所以在調用函數組件之前,當務之急是根據當前所處的階段來決定ReactCurrentDispatcher的current,這樣才可以在正確的階段調用到正確的hook函數。
export function renderWithHooks<Props, SecondAwrg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 區分是掛載還是更新過程,獲取不同的hooks函數集合
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 調用函數組件,
let children = Component(props, secondArg);
...
return children;
}
HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
,它們內部的hooks函數是不同的實現,區別之壹在於不同階段對於hooks鏈表的處理是不同的。
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
...
};
無論是初次掛載還是更新,每調用壹次hooks函數,都會產生壹個hook對象與之對應。以下是hook對象的結構。
{
baseQueue: null,
baseState: 'hook1',
memoizedState: null,
queue: null,
next: {
baseQueue: null,
baseState: null,
memoizedState: 'hook2',
next: null
queue: null
}
}
產生的hook對象依次排列,形成鏈表存儲到函數組件fiber.memoizedState上。在這個過程中,有壹個十分重要的指針:workInProgressHook,它通過記錄當前生成(更新)的hook對象,可以間接反映在組件中當前調用到哪個hook函數了。每調用壹次hook函數,就將這個指針的指向移到該hook函數產生的hook對象上。例如:
const HooksExp = () => {
const [ stateHookA, setHookA ] = useState('A')
useEffect(() => { console.log('B') })
const [ stateHookC, setHookC ] = useState('C')
return <div>Hook Example</div>
}
上面的例子中,HooksExp組件內壹共調用了三個hooks函數,分別是useState、useEffect、useState。那麽構建hook鏈表的過程,可以概括為下面這樣,重點關註workInProgressHook的指向變化。
調用useState('A'):
fiber.memoizedState: hookA
^
workInProgressHook
調用useEffect:
fiber.memoizedState: hookA -> hookB
^
workInProgressHook
調用useState('C'):
fiber.memoizedState: hookA -> hookB -> hookC
^
workInProgressHook
hook函數每次執行,都會創建它對應的hook對象,去進行下壹步的操作,比如useReducer會在hook對象上掛載更新隊列,useEffect會在hook對象上掛載effect鏈表。而創建hook對象的過程實際上也是hooks鏈表構建以及workInProgressHook指針指向更新的過程。
初次掛載時,組件上沒有任何hooks的信息,所以,這個過程主要是在fiber上創建hooks鏈表。掛載調用的是mountWorkInProgressHook
,它會創建hook並將他們連接成鏈表,同時更新workInProgressHook,最終返回新創建的hook,也就是hooks鏈表。
function mountWorkInProgressHook(): Hook {
// 創建hook對象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// workInProgressHook為null說明此時還沒有hooks鏈表,
// 將新hook對象作為第壹個元素掛載到fiber.memoizedState,
// 並將workInProgressHook指向它。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// workInProgressHook不為null說明已經有hooks鏈表,此時將
// 新的hook對象連接到鏈表後邊,並將workInProgressHook指向它。
workInProgressHook = workInProgressHook.next = hook;
}
// 返回的workInProgressHook即為新創建的hook
return workInProgressHook;
}
currentlyRenderingFiber就是workInProgress節點
我們在組件中調用hook函數,就可以獲取到hook對象,例如useState:
const HooksDispatcherOnMount: Dispatcher = {
...
useState: mountState,
...
};
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 獲取hook對象
const hook = mountWorkInProgressHook();
// 對hook對象的處理
...
return [hook.memoizedState, dispatch];
}
在更新過程中,由於存在current樹,所以workInProgress節點也就有對應的current節點。那麽自然也會有兩條hooks鏈表,分別存在於current和workInProgress節點的memorizedState屬性上。鑒於此,更新過程的hooks鏈表構建需要另壹個指針的參與:currentHook。它作為組件的workInProgressHook在上壹次更新時對應的hook對象,新的hook對象可以基於它創建。另外,也可以獲取到上次hook對象的壹些數據,例如useEffect的前後依賴項比較,前壹次的依賴項就可以通過它獲得。
currentTree
current.memoizedState = hookA -> hookB -> hookC
^
currentHook
|
workInProgress Tree |
|
workInProgress.memoizedState = hookA -> hookB
^
workInProgressHook
所以更新過程的hooks鏈表構建過程除了更新workInProgressHook指針的指向,還要更新currentHook的指向,以及盡可能復用currentHook來創建新的hook對象。
這個過程調用的是updateWorkInProgressHook
函數:
function updateWorkInProgressHook(): Hook {
// 確定nextCurrentHook的指向
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// currentHook在函數組件調用完成時會被設置為null,
// 這說明組件是剛剛開始重新渲染,剛剛開始調用第壹個hook函數。
// hooks鏈表為空
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// current節點存在,將nextCurrentHook指向current.memoizedState
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 這說明已經不是第壹次調用hook函數了,
// hooks鏈表已經有數據,nextCurrentHook指向當前的下壹個hook
nextCurrentHook = currentHook.next;
}
// 確定nextWorkInProgressHook的指向
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
// workInProgress.memoizedState在函數組件每次渲染時都會被設置成null,
// workInProgressHook在函數組件調用完成時會被設置為null,
// 所以當前的判斷分支說明現在正調用第壹個hook函數,hooks鏈表為空
// 將nextWorkInProgressHook指向workInProgress.memoizedState,為null
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 走到這個分支說明hooks鏈表已經有元素了,將nextWorkInProgressHook指向
// hooks鏈表的下壹個元素
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 依據上面的推導,nextWorkInProgressHook不為空說明hooks鏈表不為空
// 更新workInProgressHook、nextWorkInProgressHook、currentHook
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 走到這個分支說明hooks鏈表為空
// 剛剛調用第壹個hook函數,基於currentHook新建壹個hook對象,
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 依據情況構建hooks鏈表,更新workInProgressHook指針
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
通過本文我們了解到,函數組件內的hooks函數調用是會形成壹個hooks鏈表的,這個鏈表會掛載到函數組件對應fiber的memoizedState屬性上。這為我們後續hooks函數的講解奠定了基礎。