文章的 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) {
// This is the first hook in the list
/**
* 這裡可以看到,fiber 中的 memoizedState 是用來存儲 hooks 鏈表的
*/
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
/**
* 後續在調用其他 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 list is the list that belongs to the current fiber 可知,current hook 指向了 current fiber 樹上的 hooks 鏈表
// 畢竟更新操作時,必然已經存在了 current 樹
// currentHook在函數元件調用完成時會被設置為null因此可以用來確認是否是剛開始重新渲染
if (currentHook === null) {
/**
* 根據註釋 The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress 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) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current 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,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
代碼冗長,指針又跳來跳去,看懂的最好辦法還是自己調試一下。這裡最重要的信息其實就是,在更新時會借 current 節點上的 hooks 鏈表去生成新的 hooks 鏈表。至於如何更新狀態不在本文內介紹。
總結#
- 每次調用函數式元件都會生成一個 hooks 鏈表掛載在對應 fiber 的 memoizedState 上。