
大家好,我是前端西瓜哥。
今天我們從源碼來理解 React Hook 是如何工作的。
React Hook 是 React 16.8 后新加入的黑魔法,讓我們可以 在函數組件內保存內部狀態。
Hook 的優勢:
- 比組件更小粒度的復用,之前復用需要用 Mixin 或 高階組件(HOC,一個能夠返回組件的組件)進行封裝,前者依賴關系隱式導致難以維護,后者粒度過大、嵌套過深。
- 將處理同一個邏輯的業務代碼放在一起,讓代碼可以更好維護。如果是類組件,得放各個生命周期函數中,邏輯會很分散。
- 類組件的 class 寫法容易寫錯,一不小心 this 就指向錯誤,沒錯就是說事件響應函數你。另外讀取值也麻煩,要寫很長的this.state.count。
- 擁抱函數式編程,這是 React 團隊所提倡的編程寫法。
一些全局變量
在講解源碼之前,先認識一些 重要的全局變量:
currentlyRenderingFiber:正在處理的函數組件對應 fiber。在執行 useState 等 hook 時,需要通過它知道當前 hook 對應哪個 fiber。
workInProgressHook:掛載時正在處理的 hook 對象。我們會沿著 workInProcess.memoizedState 鏈表一個個往下走,這個 workInProgressHook 就是該鏈表的指針。
currentHook:舊的 fiber 的 hooks 鏈表(current.memorizedState)指針。
ReactCurrentDispatcher:全局對象,是一個 hook 調度器對象,其下有 useState、useEffect 等方法,是我們業務代碼中 hook 底層調用的方法。ReactCurrentDispatcher 有三種:
- ContextOnlyDispatcher:所有方法都會拋出錯誤,用于防止開發者在調用函數組件的其他時機調用 React Hook;
- HooksDispatcherOnMount:掛載階段用。比如它的 useState 要將初始值保存起來;
- HooksDispatcherOnUpdate:更新階段用。比如它的 useState 會無視傳入的初始值,而是從鏈表中取出值。
renderWithHooks
構建函數實例是在 renderWithHooks 方法中進行的。
主要邏輯為:
- workInProgress 賦值給全局變量 currentlyRenderingFiber,之后執行 hook 就能知道是給哪個組件更新狀態了。
- 選擇 hook 調度器:根據是掛載還是更新階段,ReactCurrentDispatcher 設置為對應 hook 調度器。
- 調用函數組件,進行 render。函數組件內部會調用 Hook,并返回 ReactElement。
- 重置全局變量,比如 currentlyRenderingFiber 設置回 null;ReactCurrentDispatcher 還原為 ContextOnlyDispatcher,防止在錯誤時機使用 Hook。
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes
) {
renderLanes = nextRenderLanes;
// 1. 將 workInProgress 賦值給全局變量 currentlyRenderingFiber
// 這樣我們在調用 Hook 時就能知道對應的 fiber 是誰
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 2. 根據是掛載還是更新階段,選擇對應 hook 調度器
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 3. 調用函數組件,里面執行各種 React Hook,并返回 ReactElement
let children = Component(props, secondArg);
// 4. hook 調度器還原為 ContextOnlyDispatcher
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
// 將一些全局變量進行重置
renderLanes = NoLanes;
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
// Hook 數量比上次少,對不上,報錯
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}
return children;
}
下面看看在函數組件一些常見 Hook 是如何工作的。
useState
首先討論 狀態 Hook 中最常見的一種:useState。
掛載階段(狀態初始化)
useState 在掛載階段,調用的是 HooksDispatcherOnMount.useState,也就是 mountState。
- 創建新的 hook 空對象,掛到 workInProcess.memorizedState 隊列上(mountWorkInProgressHook 方法)。
- dispatchSetState 綁定對應 fiber 和 queue,方便以后 setState 快速找到相關對象,最后返回狀態值和更新狀態方法。
function mountState(initialState) {
// 1. 創建一個 hook 對象,并添加到 workInProcess.memoizedState 鏈表上
const hook = mountWorkInProgressHook();
// useState 傳入的可能是個函數,要調用一下拿到初始值
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
// 更新 state 的方法
const dispatch = queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);
// 返回我們經常用的 [state, setState]
return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook 實現:
function mountWorkInProgressHook() {
// 新的 hook 空對象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// 給 memoizedState 鏈表加節點的邏輯
// 寫過單鏈表的會比較理解,頭節點要特殊處理
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
更新狀態操作(setState)
之前 mountState 時,我們返回了一個綁定了 fiber、queue 參數的 dispatchSetState。setState 更新操作調用的正是這個 dispatchSetState。
第一個 setState 在被調用時會立即計算新狀態,這是為了 做新舊 state 對比,決定是否更新組件。如果對比發現狀態沒變,繼續計算下一個 setState 的新狀態,直到找到為止。如果沒找到,就不進行更新。
其后的 setState 則不會計算,等到組件重新 render 再計算。
為對比新舊狀態計算出來的狀態值,會保存到 update.eagerState,并將 update.hasEagerState 設置為 true,之后更新時通過它來直接拿到計算后的最新值。
dispatchSetState 會拿到對應的 fiber、queue(對應 hook 的 queue)、action(新的狀態)。
- 創建一個 update 空對象;
- 計算出最新狀態,放入到 update.egerState。
- 對比新舊狀態是否相同(使用 Object.is 對比)。相同就不更新了,結束。不相同,進行后續的操作。
- 將 update 放到 queue.interleaved 或 concurrentQueues 鏈表上(.new 和 .old 文件的邏輯差得有點多),之后更新階段會搬到 queue.pending。
- 將當前 fiber 的 lanes 設置為 SyncLane,這樣后面的 setState 就不會立刻計算最新狀態了,而是在更新階段才計算。
- 接著是調度更新(scheduleUpdateOnFiber),讓調度器進行調度,執行更新操作。
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
// 創建一個 update 更新對象
const update = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
if (isRenderPhaseUpdate(fiber)) {
// 渲染階段更新,先不討論這種特殊情況
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
// 第二次 setState 時,fiber.lanes 為 SyncLane
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState;
// 計算新狀態
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 對比新舊狀態是否不同
if (is(eagerState, currentState)) {
// 狀態沒改變,當前 setState 無效,return 結束,無事發生
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
// 將 update 加到 queue 鏈表末尾
// 將 fiber 標記為 SyncLane
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 調度 fiber 更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
更新階段(獲取最新狀態)
我們先了解一個前置知識:useState 是特殊的 useReducer。
useState 本質上在使用 useReducer,在 React 源碼層提供了特殊的名為 basicStateReducer 的 reducer,后面源碼解析中會看到它。
const _useState = (initalVal) => {
return React.useReducer(
function (preState, action) {
// action 對應 setState 傳入的最新狀態
// 如果不是函數,直接更新為最新狀態
// 如果是函數,傳入 preState 并調用函數,并將返回值作為最新狀態
return typeof action === 'function' ? action(preState) : action;
},
initalVal
)
}
回到正題。
useState 在更新階段會拿到上一次的狀態值,此階段調用的是 HooksDispatcherOnUpdate.useState,也就是 updateState。
updateState 會調用 updateReducer(useReducer 更新階段也用這個),這也是為什么我說 setState 是特殊 useReducer 的原因。
updateReducer 主要工作有兩個:
- 從 current.memorizedState 拷貝 hook 到 workInProcess 下(updateWorkInProgressHook 方法)。
- 將 hook.queue.pending 隊列合并到 currentHook.baseQueue 下,然后遍歷隊列中的 update 對象,使用 action 和 reducer 計算出最新的狀態,更新到 hook 上,最后返回新狀態和新 setState。
function updateState(initialState) {
// 實際用的是 updateReducer
return updateReducer(basicStateReducer);
}
// reducer 函數
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
// setReducer 更新階段對應的 updateReducer
function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷貝 hook(current -> workInProcess),并返回這個 hook -----
const hook = updateWorkInProgressHook();
// ----- 【2】 讀取隊列,計算出最新狀態,更新 hook 的狀態 -----
// ...
}
先看看 updateWorkInProgressHook 方法。
該方法中,currentHook 設置為 current.memoizedState 鏈表的下一個 hook,拷貝它到 currentlyRenderingFiber.memoizedState 鏈表上,返回這個 hook。
function updateWorkInProgressHook() {
// 1. 移動 currentHook 指針
//(來自 current.memoizedState 鏈表)
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 2. 移動 workInProgressHook 指針
//(來自 currentlyRenderingFiber.memoizedState 鏈表)
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 這種情況為 “渲染時更新邏輯”(在 render 時調用了 setState)
// 為了更聚焦普通情況,這里不討論
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 3. 渲染時不更新,nextWorkInProgressHook 就一定是 null
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null // next 就不拷貝了
};
// 4. 經典單鏈表末尾加節點寫法
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 5. 返回拷貝 hook 對象
return workInProgressHook;
}
拿到拷貝后的 hook,就可以計算新狀態值了。
首先將 hook.queue.pending 隊列合并到 currentHook.baseQueue 下。該隊列包含了一系列 update 對象(因為可能調用了多次 setState),里面保存有 setState 傳入的最新狀態值(函數或其他值)。
然后遍歷 update 計算出最新狀態,保存回 hook,并返回最新狀態值和 setState 方法。
function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷貝 hook(current -> workInProcess),并返回這個 hook ----
const hook = updateWorkInProgressHook();
// ----- 【2】 讀取隊列,計算出最新狀態,更新 hook 的狀態 -----
// 取出 hook.queue 鏈表,添加到 current.baseQueue 末尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 處理更新隊列
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 循環,根據 baseQueue 鏈表下的 update 對象計算新狀態
do {
// 刪掉了一些跳過更新的邏輯
if (update.hasEagerState) {
// 為了對比新舊狀態來決定是否更新,所計算的新狀態。
// 如果不同,給 update.hasEagerState 設置為 true
// 新狀態賦值給 update.eagerState
newState = update.eagerState;
} else {
// 計算新狀態
const action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新 hook 狀態
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
useEffect
有些邏輯類似 useState,比如創建 hook 的 mountWorkInProgressHook 方法實現,所以一些重復邏輯就不說了,直奔核心。
掛載階段
核心函數是 mountEffectImpl。
- 【mountWorkInProgressHook】創建一個 hook 空對象,放到 workInProcess.memorizedState 下。
- 【pushEffect】創建 effect,添加到 當前 fiber 的 updateQueue 的鏈表上,并將該 effect 賦值給 hook.memoizedState。
mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// create 和 deps 是 useEffect 接受的兩個參數
// 1. 新建 hook 對象
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
// 2. 新建 effect 對象,放到 hook.memoizedState 下。
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
pushEffect 實現:
function pushEffect(tag, create, destroy, deps) {
// 創建 effect
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null
};
var componentUpdateQueue = currentlyRenderingFiber.updateQueue;
// 添加到當前 fiber.updateQueue 下。
// updateQueue.laseEffect 保存鏈表的最后一個 effect
// 且使用的是環形鏈表,通過 updateQueue.laseEffect.next 得到鏈表頭節點
// 如果 updateQueue 為 null,初始化一個空的 updateQueue 對象
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 往 updateQueue.lastEffect 鏈表上添加 effect 對象。
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
更新階段
核心實現在 updateEffectImpl。
- 從 current 拷貝 hook 到 workInProcess。
- 對比新舊依賴項 deps,如果沒改變,也創建 effect 加隊列上(但最終不會執行),結束;否則繼續。
- 給當前 fiber 打上 PassiveEffect,表示有 useEffect 的回調要執行。
- 創建 effect ,tag 補上加 HookHasEffect,然后加隊列上,后面會執行。
function updateEffect(create, dep) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// hookFlags 此時為 PassiveEffect(代表)
// 1. 從 current 拷貝 hook 到 workInProcess
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 存在依賴項
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 依賴項沒有改變,結束
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 還是會新建 effect,更新 updateQueue 和 memorizedState
// 但 tag 只是 PassiveEffect,后面遍歷時不會執行
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 當前 fiber 打上 PassiveEffect 標記
// 該標記表示存在需要執行的 useEffect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
// 相比上面依賴項不變的情況,這里加了 HookHasEffect 標簽
// 之后根據 fiber.updateQueue 會執行
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
我們看下依賴項對比算法 areHookInputsEqual 的細節,它同時遍歷到新舊依賴項最長的尾部,進行 Object.is 對比。在空數組情況下,這個比較一定返回 true,所以能模擬 componentDidMount / Unmount 的效果。
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
useEffect 的 create 和 destroy 的執行時機
當 commit 階段結束后,useEffect 的 create 和 destroy 會被 Schedule 調度器異步調度執行。
fiber.updateQueue 下的 effect 會按順序取出,然后一個個執行。
function commitPassiveUnmountOnFiber(finishedWork) {
// 執行所有 tag 為 HookPassive | HookHasEffect 的 effect 的 destroy
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
// 執行所有 tag 為 HookPassive | HookHasEffect 的 effect 的 create
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
之前依賴項相同的話,雖然也創建 effect,但它的 tag 對不上,是不會執行的。
一些面試題的簡單回答
1、React Hooks 為什么不能寫在條件語句中?
我們要保證 React Hooks 的順序一致。
函數組件的狀態是保存在 fiber.memorizedState 中的。它是一個鏈表,保存調用 Hook 生成的 hook 對象,這些對象保存著狀態值。當更新時,我們每調用一個 Hook,其實就是從 fiber.memorizedState 鏈表中讀取下一個 hook,取出它的狀態。
如果順序不一致了或者數量不一致了,就會導致錯誤,取出了一個其他 Hook 對應的狀態值。
2、React Hooks 為什么必須在函數組件內部執行?React 如何能夠監聽 React Hooks 在外部執行并拋出異常??
Hooks 底層調用的是一個全局變量 ReactCurrentDispatcher 的一系列方法。
這個全局變量會在不同階段設置為不同的對象。render 過程中,掛載階段設置為 HooksDispatcherOnMount,更新階段設置為 HooksDispatcherOnUpdate。它們會讀取 currentlyRenderingFiber 全局變量,這個全局變量代表正在處理的 fiber,讀取它進行一些設置狀態和讀取狀態等操作。
在 render 階段外,會設置為 ContextOnlyDispatcher,這個對象下所有方法都會拋出錯誤,因為此時不存在正常處理的 fiber,使用時機是并不對。
結尾
本文只講了狀態 Hook 代表 useState,和 副作用 Hook 代表 useEffect,其他 Hook 其實也差不多。