全網最簡單的React Hooks源碼解析!
前言
從React Hooks發(fā)布以來,整個社區(qū)都以積極的態(tài)度去擁抱它、學習它。期間也涌現了很多關于React Hooks 源碼解析的文章。本文就以筆者自己的角度來寫一篇屬于自己的文章吧。希望可以深入淺出、圖文并茂的幫助大家對React Hooks的實現原理進行學習與理解。本文將以文字、代碼、圖畫的形式來呈現內容。主要對常用Hooks中的 useState、useReducer、useEffect 進行學習,盡可能的揭開Hooks的面紗。
使用Hooks時的疑惑
Hooks的面世讓我們的Function Component逐步擁有了對標Class Component的特性,比如私有狀態(tài),生命周期函數等。useState與useReducer這兩個Hooks讓我們可以在 Function Component里使用到私有狀態(tài)。而useState其實就是閹割版的useReducer,這也是我那它們兩個放在一起講的原因。應用一下官方的例子:
function?PersionInfo?({initialAge,initialName})?{
??const?[age,?setAge]?=?useState(initialAge);
??const?[name,?setName]?=?useState(initialName);
??return?(
????<>
??????Age:?{age},?Name:?{name}
??????
????>
??);
}
useState 我們可以初始化一個私有狀態(tài),它會返回這個狀態(tài)的最新值和一個用來更新狀態(tài)的方法。而useReducer則是針對更復雜的狀態(tài)管理場景:
const?initialState?=?{age:?0,?name:?'Dan'};
function?reducer(state,?action)?{
??switch?(action.type)?{
????case?'increment':
??????return?{...state,?age:?state.age?+?action.age};
????case?'decrement':
??????return?{...state,?age:?state.age?-?action.age};
????default:
??????throw?new?Error();
??}
}
function?PersionInfo()?{
??const?[state,?dispatch]?=?useReducer(reducer,?initialState);
??return?(
????<>
??????Age:?{state.age},?Name:?{state.name}
??????
??????
????>
??);
}
同樣也是返回當前最新的狀態(tài),并返回一個用來更新數據的方法。在使用這兩個方法的時候也許我們會想過這樣的問題:
??const?[age,?setAge]?=?useState(initialAge);
??const?[name,?setName]?=?useState(initialName);
React內部是怎么區(qū)分這兩個狀態(tài)的呢?
Function Component 不像 Class Component那樣可以將私有狀態(tài)掛載到類實例中并通過對應的key來指向對應的狀態(tài),而且每次的頁面的刷新或者說組件的重新渲染都會使得 Function 重新執(zhí)行一遍。所以React中必定有一種機制來區(qū)分這些Hooks。
?const?[age,?setAge]?=?useState(initialAge);
?//?或
?const?[state,?dispatch]?=?useReducer(reducer,?initialState);
另一個問題就是React是如何在每次重新渲染之后都能返回最新的狀態(tài)?
Class Component因為自身的特點可以將私有狀態(tài)持久化的掛載到類實例上,每時每刻保存的都是最新的值。而 Function Component 由于本質就是一個函數,并且每次渲染都會重新執(zhí)行。所以React必定擁有某種機制去記住每一次的更新操作,并最終得出最新的值返回。當然我們還會有其他的一些問題,比如這些狀態(tài)究竟存放在哪?為什么只能在函數頂層使用Hooks而不能在條件語句等里面使用Hooks?
答案盡在源碼之中
我們先來了解useState以及useReducer的源碼實現,并從中解答我們在使用Hooks時的種種疑惑。首先我們從源頭開始:
import?React,?{?useState?}?from?'react';
在項目中我們通常會以這種方式來引入useState方法,被我們引入的這個useState方法是什么樣子的呢?其實這個方法就在源碼 packages/react/src/ReactHook.js 中。
//?packages/react/src/ReactHook.js
import?ReactCurrentDispatcher?from?'./ReactCurrentDispatcher';
function?resolveDispatcher()?{
??const?dispatcher?=?ReactCurrentDispatcher.current;
??//?...?
??return?dispatcher;
}
//?我們代碼中引入的useState方法
export?function?useState(initialState)?{
??const?dispatcher?=?resolveDispatcher();
??return?dispatcher.useState(initialState)
}
從源碼中可以看到,我們調用的其實是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那么我們繼續(xù)前往ReactCurrentDispatcher.js文件:
import?type?{Dispacther}?from?'react-reconciler/src/ReactFiberHooks';
const?ReactCurrentDispatcher?=?{
??current:?(null:?null?|?Dispatcher),
};
export?default?ReactCurrentDispatcher;
好吧,它繼續(xù)將我們帶向 react-reconciler/src/ReactFiberHooks.js這個文件。那么我們繼續(xù)前往這個文件。
//?react-reconciler/src/ReactFiberHooks.js
export?type?Dispatcher?=?{
??useState(initialState:?(()?=>?S)?|?S):?[S,?Dispatch>],
??useReducer(
????reducer:?(S,?A)?=>?S,
????initialArg:?I,
????init?:?(I)?=>?S,
??):?[S,?Dispatch],
??useEffect(
????create:?()?=>?(()?=>?void)?|?void,
????deps:?Array?|?void?|?null,
??):?void,
??//?其他hooks類型定義
}
兜兜轉轉我們終于清楚了React Hooks 的源碼就放 react-reconciler/src/ReactFiberHooks.js 目錄下面。在這里如上圖所示我們可以看到有每個Hooks的類型定義。同時我們也可以看到Hooks的具體實現,大家可以多看看這個文件。首先我們注意到,我們大部分的Hooks都有兩個定義:
//?react-reconciler/src/ReactFiberHooks.js
//?Mount?階段Hooks的定義
const?HooksDispatcherOnMount:?Dispatcher?=?{
??useEffect:?mountEffect,
??useReducer:?mountReducer,
??useState:?mountState,
?//?其他Hooks
};
//?Update階段Hooks的定義
const?HooksDispatcherOnUpdate:?Dispatcher?=?{
??useEffect:?updateEffect,
??useReducer:?updateReducer,
??useState:?updateState,
??//?其他Hooks
};
從這里可以看出,我們的Hooks在Mount階段和Update階段的邏輯是不一樣的。在Mount階段和Update階段他們是兩個不同的定義。我們先來看Mount階段的邏輯。在看之前我們先思考一些問題。React Hooks需要在Mount階段做什么呢?就拿我們的useState和useReducer來說:
我們需要初始化狀態(tài),并返回修改狀態(tài)的方法,這是最基本的。 我們要區(qū)分管理每個Hooks。 提供一個數據結構去存放更新邏輯,以便后續(xù)每次更新可以拿到最新的值。
我們一下React的實現,先來看mountState的實現。
//?react-reconciler/src/ReactFiberHooks.js
function?mountState?(initialState)?{
??//?獲取當前的Hook節(jié)點,同時將當前Hook添加到Hook鏈表中
??const?hook?=?mountWorkInProgressHook();
??if?(typeof?initialState?===?'function')?{
????initialState?=?initialState();
??}
??hook.memoizedState?=?hook.baseState?=?initialState;
??//?聲明一個鏈表來存放更新
??const?queue?=?(hook.queue?=?{
????last:?null,
????dispatch:?null,
????lastRenderedReducer,
????lastRenderedState,
??});
??//?返回一個dispatch方法用來修改狀態(tài),并將此次更新添加update鏈表中
??const?dispatch?=?(queue.dispatch?=?(dispatchAction.bind(
????null,
????currentlyRenderingFiber,
????queue,
??)));
??//?返回當前狀態(tài)和修改狀態(tài)的方法?
??return?[hook.memoizedState,?dispatch];
}
區(qū)分管理Hooks
關于第一件事,初始化狀態(tài)并返回狀態(tài)和更新狀態(tài)的方法。這個沒有問題,源碼也很清晰利用initialState來初始化狀態(tài),并且返回了狀態(tài)和對應更新方法 return [hook.memoizedState, dispatch]。那么我們來看看React是如何區(qū)分不同的Hooks的,這里我們可以從 mountState 里的 mountWorkInProgressHook方法和Hook的類型定義中找到答案。
//?react-reconciler/src/ReactFiberHooks.js
export?type?Hook?=?{
??memoizedState:?any,
??baseState:?any,
??baseUpdate:?Update?|?null,
??queue:?UpdateQueue?|?null,
??next:?Hook?|?null,??//?指向下一個Hook
};
首先從Hook的類型定義中就可以看到,React 對Hooks的定義是鏈表。也就是說我們組件里使用到的Hooks是通過鏈表來聯(lián)系的,上一個Hooks的next指向下一個Hooks。這些Hooks節(jié)點是怎么利用鏈表數據結構串聯(lián)在一起的呢?相關邏輯就在每個具體mount 階段 Hooks函數調用的 mountWorkInProgressHook方法里:
//?react-reconciler/src/ReactFiberHooks.js
function?mountWorkInProgressHook():?Hook?{
??const?hook:?Hook?=?{
????memoizedState:?null,
????baseState:?null,
????queue:?null,
????baseUpdate:?null,
????next:?null,
??};
??if?(workInProgressHook?===?null)?{
????//?當前workInProgressHook鏈表為空的話,
????//?將當前Hook作為第一個Hook
????firstWorkInProgressHook?=?workInProgressHook?=?hook;
??}?else?{
????//?否則將當前Hook添加到Hook鏈表的末尾
????workInProgressHook?=?workInProgressHook.next?=?hook;
??}
??return?workInProgressHook;
}
在mount階段,每當我們調用Hooks方法,比如useState,mountState就會調用mountWorkInProgressHook 來創(chuàng)建一個Hook節(jié)點,并把它添加到Hooks鏈表上。比如我們的這個例子:
??const?[age,?setAge]?=?useState(initialAge);
??const?[name,?setName]?=?useState(initialName);
??useEffect(()?=>?{})
那么在mount階段,就會生產如下圖這樣的單鏈表:

返回最新的值
而關于第三件事,useState和useReducer都是使用了一個queue鏈表來存放每一次的更新。以便后面的update階段可以返回最新的狀態(tài)。每次我們調用dispatchAction方法的時候,就會形成一個新的updata對象,添加到queue鏈表上,而且這個是一個循環(huán)鏈表??梢钥匆幌?dispatchAction 方法的實現:
//?react-reconciler/src/ReactFiberHooks.js
//?去除特殊情況和與fiber相關的邏輯
function?dispatchAction(fiber,queue,action,)?{
????const?update?=?{
??????action,
??????next:?null,
????};
????//?將update對象添加到循環(huán)鏈表中
????const?last?=?queue.last;
????if?(last?===?null)?{
??????//?鏈表為空,將當前更新作為第一個,并保持循環(huán)
??????update.next?=?update;
????}?else?{
??????const?first?=?last.next;
??????if?(first?!==?null)?{
????????//?在最新的update對象后面插入新的update對象
????????update.next?=?first;
??????}
??????last.next?=?update;
????}
????//?將表頭保持在最新的update對象上
????queue.last?=?update;
???//?進行調度工作
????scheduleWork();
}
也就是我們每次執(zhí)行dispatchAction方法,比如setAge或setName。就會創(chuàng)建一個保存著此次更新信息的update對象,添加到更新鏈表queue上。然后每個Hooks節(jié)點就會有自己的一個queque。比如假設我們執(zhí)行了下面幾個語句:
setAge(19);
setAge(20);
setAge(21);
那么我們的Hooks鏈表就會變成這樣:

在Hooks節(jié)點上面,會如上圖那樣,通過鏈表來存放所有的歷史更新操作。以便在update階段可以通過這些更新獲取到最新的值返回給我們。這就是在第一次調用useState或useReducer之后,每次更新都能返回最新值的原因。再來看看mountReducer,你會發(fā)現和mountState幾乎一摸一樣,只是狀態(tài)的初始化邏輯有那么一點區(qū)別。畢竟useState其實就是閹割版的useReducer。這里就不詳細介紹mountReducer了。
//?react-reconciler/src/ReactFiberHooks.js
function?mountReducer(reducer,?initialArg,?init,)?{
??//?獲取當前的Hook節(jié)點,同時將當前Hook添加到Hook鏈表中
??const?hook?=?mountWorkInProgressHook();
??let?initialState;
??//?初始化
??if?(init?!==?undefined)?{
????initialState?=?init(initialArg);
??}?else?{
????initialState?=?initialArg?;
??}
??hook.memoizedState?=?hook.baseState?=?initialState;
??//?存放更新對象的鏈表
??const?queue?=?(hook.queue?=?{
????last:?null,
????dispatch:?null,
????lastRenderedReducer:?reducer,
????lastRenderedState:?(initialState:?any),
??});
??//?返回一個dispatch方法用來修改狀態(tài),并將此次更新添加update鏈表中
??const?dispatch?=?(queue.dispatch?=?(dispatchAction.bind(
????null,
????currentlyRenderingFiber,
????queue,
??)));
?//?返回狀態(tài)和修改狀態(tài)的方法
??return?[hook.memoizedState,?dispatch];
}
然后我們來看看update階段,也就是看一下我們的useState或useReducer是如何利用現有的信息,去給我們返回最新的最正確的值的。先來看一下useState在update階段的代碼也就是updateState:
//?react-reconciler/src/ReactFiberHooks.js
function?updateState(initialState)?{
??return?updateReducer(basicStateReducer,?initialState);
}
可以看到,updateState底層調用的其實就會死updateReducer,因為我們調用useState的時候,并不會傳入reducer,所以這里會默認傳遞一個basicStateReducer進去。我們先看看這個basicStateReducer:
//?react-reconciler/src/ReactFiberHooks.js
function?basicStateReducer(state,?action){
??return?typeof?action?===?'function'???action(state)?:?action;
}?
在使用useState(action)的時候,action通常會是一個值,而不是一個方法。所以baseStateReducer要做的其實就是將這個action返回。來繼續(xù)看一下updateReducer的邏輯:
//?react-reconciler/src/ReactFiberHooks.js
//?去掉與fiber有關的邏輯
function?updateReducer(reducer,initialArg,init)?{
??const?hook?=?updateWorkInProgressHook();
??const?queue?=?hook.queue;
??//?拿到更新列表的表頭
??const?last?=?queue.last;
??//?獲取最早的那個update對象
??first?=?last?!==?null???last.next?:?null;
??if?(first?!==?null)?{
????let?newState;
????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?=?queue.dispatch;
??//?返回最新的狀態(tài)和修改狀態(tài)的方法
??return?[hook.memoizedState,?dispatch];
}
在update階段,也就是我們組件第二次第三次。。執(zhí)行到useState或useReducer的時候,會遍歷update對象循環(huán)鏈表,執(zhí)行每一次更新去計算出最新的狀態(tài)來返回,以保證我們每次刷新組件都能拿到當前最新的狀態(tài)。useState的reducer是baseStateReducer,因為傳入的update.action為值,所以會直接返回update.action,而useReducer 的reducer是用戶定義的reducer,所以會根據傳入的action和每次循環(huán)得到的newState逐步計算出最新的狀態(tài)。

useState/useReducer 小總結
看到這里我們在回頭看看最初的一些疑問:
React 如何管理區(qū)分Hooks?
React通過單鏈表來管理Hooks 按Hooks的執(zhí)行順序依次將Hook節(jié)點添加到鏈表中
useState和useReducer如何在每次渲染時,返回最新的值?
每個Hook節(jié)點通過循環(huán)鏈表記住所有的更新操作 在update階段會依次執(zhí)行update循環(huán)鏈表中的所有更新操作,最終拿到最新的state返回
為什么不能在條件語句等中使用Hooks?
鏈表!
比如如圖所示,我們在mount階段調用了useState('A'), useState('B'), useState('C'),如果我們將useState('B') 放在條件語句內執(zhí)行,并且在update階段中因為不滿足條件而沒有執(zhí)行的話,那么沒法正確的重Hooks鏈表中獲取信息。React也會給我們報錯。
Hooks鏈表放在哪?
好的,現在我們已經了解了React 通過鏈表來管理 Hooks,同時也是通過一個循環(huán)鏈表來存放每一次的更新操作,得以在每次組件更新的時候可以計算出最新的狀態(tài)返回給我們。那么我們這個Hooks鏈表又存放在那里呢?理所當然的我們需要將它存放到一個跟當前組件相對于的地方。那么很明顯這個與組件一一對應的地方就是我們的FiberNode。
如圖所示,組件構建的Hooks鏈表會掛載到FiberNode節(jié)點的memoizedState上面去。

useEffect
看到這,相信你已經對Hooks的源碼實現模式已經有一定的了解了,所以你嘗試去看一下Effect的實現你會一下子就看懂。首先我們先回憶一下useEffect是怎么樣工作的?
function?PersionInfo?()?{
??const?[age,?setAge]?=?useState(18);
??useEffect(()?=>{
??????console.log(age)
??},?[age])
?const?[name,?setName]?=?useState('Dan');
?useEffect(()?=>{
??????console.log(name)
??},?[name])
??return?(
????<>
??????...
????>
??);
}
PersionInfo組件第一次渲染的時候會在控制臺輸出age和name,在后面組件的每次update中,如果useEffect中的deps依賴的值發(fā)生了變化的話,也會在控制臺中輸出對應的狀態(tài),同時在unmount的時候就會執(zhí)行清除函數(如果有)。React中是怎么實現的呢?其實很簡單,在FiberNode中通過一個updateQueue來存放所有的effect,然后在每次渲染之后依次執(zhí)行所有需要執(zhí)行的effect。useEffect 也分為mountEffect和updateEffect
mountEffect
//?react-reconciler/src/ReactFiberHooks.js
//?簡化去掉特殊邏輯
function?mountEffect(?create,deps,)?{
??return?mountEffectImpl(
????create,
????deps,
??);
}
function?mountEffectImpl(fiberEffectTag,?hookEffectTag,?create,?deps)?{
??//?獲取當前Hook,并把當前Hook添加到Hook鏈表
??const?hook?=?mountWorkInProgressHook();
??const?nextDeps?=?deps?===?undefined???null?:?deps;
??//?將當前effect保存到Hook節(jié)點的memoizedState屬性上,
??//?以及添加到fiberNode的updateQueue上
??hook.memoizedState?=?pushEffect(hookEffectTag,?create,?undefined,?nextDeps);
}
function?pushEffect(tag,?create,?destroy,?deps)?{
??const?effect:?Effect?=?{
????tag,
????create,
????destroy,
????deps,
????next:?(null:?any),
??};
??//?componentUpdateQueue?會被掛載到fiberNode的updateQueue上
??if?(componentUpdateQueue?===?null)?{
????//?如果當前Queue為空,將當前effect作為第一個節(jié)點
????componentUpdateQueue?=?createFunctionComponentUpdateQueue();
???//?保持循環(huán)
????componentUpdateQueue.lastEffect?=?effect.next?=?effect;
??}?else?{
????//?否則,添加到當前的Queue鏈表中
????const?lastEffect?=?componentUpdateQueue.lastEffect;
????if?(lastEffect?===?null)?{
??????componentUpdateQueue.lastEffect?=?effect.next?=?effect;
????}?else?{
??????const?firstEffect?=?lastEffect.next;
??????lastEffect.next?=?effect;
??????effect.next?=?firstEffect;
??????componentUpdateQueue.lastEffect?=?effect;
????}
??}
??return?effect;?
}
可以看到在mount階段,useEffect做的事情就是將自己的effect添加到了componentUpdateQueue上。這個componentUpdateQueue會在renderWithHooks方法中賦值到fiberNode的updateQueue上。
//?react-reconciler/src/ReactFiberHooks.js
//?簡化去掉特殊邏輯
export?function?renderWithHooks()?{
???const?renderedWork?=?currentlyRenderingFiber;
???renderedWork.updateQueue?=?componentUpdateQueue;
}
也就是在mount階段我們所有的effect都以鏈表的形式被掛載到了fiberNode上。然后在組件渲染完畢之后,React就會執(zhí)行updateQueue中的所有方法。

updateEffect
//?react-reconciler/src/ReactFiberHooks.js
//?簡化去掉特殊邏輯
function?updateEffect(create,deps){
??return?updateEffectImpl(
????create,
????deps,
??);
}
function?updateEffectImpl(fiberEffectTag,?hookEffectTag,?create,?deps){
??//?獲取當前Hook節(jié)點,并把它添加到Hook鏈表
??const?hook?=?updateWorkInProgressHook();
??//?依賴?
??const?nextDeps?=?deps?===?undefined???null?:?deps;
?//?清除函數
??let?destroy?=?undefined;
??if?(currentHook?!==?null)?{
????//?拿到前一次渲染該Hook節(jié)點的effect
????const?prevEffect?=?currentHook.memoizedState;
????destroy?=?prevEffect.destroy;
????if?(nextDeps?!==?null)?{
??????const?prevDeps?=?prevEffect.deps;
??????//?對比deps依賴
??????if?(areHookInputsEqual(nextDeps,?prevDeps))?{
????????//?如果依賴沒有變化,就會打上NoHookEffect?tag,在commit階段會跳過此
????????//?effect的執(zhí)行
????????pushEffect(NoHookEffect,?create,?destroy,?nextDeps);
????????return;
??????}
????}
??}
??hook.memoizedState?=?pushEffect(hookEffectTag,?create,?destroy,?nextDeps);
}
update階段和mount階段類似,只不過這次會考慮effect 的依賴deps,如果此次更新effect的依賴沒有變化的話,就會被打上NoHookEffect標簽,最后會在commit階段跳過改effect的執(zhí)行。
function?commitHookEffectList(unmountTag,mountTag,finishedWork)?{
??const?updateQueue?=?finishedWork.updateQueue;
??let?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????if?((effect.tag?&?unmountTag)?!==?NoHookEffect)?{
????????//?Unmount?階段執(zhí)行tag?!==?NoHookEffect的effect的清除函數?(如果有的話)
????????const?destroy?=?effect.destroy;
????????effect.destroy?=?undefined;
????????if?(destroy?!==?undefined)?{
??????????destroy();
????????}
??????}
??????if?((effect.tag?&?mountTag)?!==?NoHookEffect)?{
????????//?Mount?階段執(zhí)行所有tag?!==?NoHookEffect的effect.create,
????????//?我們的清除函數(如果有)會被返回給destroy屬性,一遍unmount執(zhí)行
????????const?create?=?effect.create;
????????effect.destroy?=?create();
??????}
??????effect?=?effect.next;
????}?while?(effect?!==?firstEffect);
??}
}
useEffect 小總結

useEffect做了什么?
FiberNdoe節(jié)點中會又一個updateQueue鏈表來存放所有的本次渲染需要執(zhí)行的effect。 mountEffect階段和updateEffect階段會把effect 掛載到updateQueue上。 updateEffect階段,deps沒有改變的effect會被打上NoHookEffect tag,commit階段會跳過該Effect。
到此為止,useState/useReducer/useEffect源碼也閱讀完畢了,相信有了這些基礎,剩下的Hooks的源碼閱讀不會成問題,最后放上完整圖示:

