背景
React 16.8 引入了 Hook 的概念捻爷,一直以來都沒時間仔細(xì)研究它的實(shí)現(xiàn)方式雹顺,
本文從一個最簡單的例子出發(fā)弊仪,記錄了 react@16.13.1 的 useState 源碼邏輯。
概覽
示例代碼:github: debug-react
function App() {
debugger;
const [state, setState] = useState(0);
debugger;
const increment = () => {
debugger;
setState(s => {
debugger;
return s + 1;
});
debugger;
setState(s => {
debugger;
return s + 2;
});
debugger;
};
debugger;
return <button onClick={increment}>{state}</button>;
}
以上代碼中龄毡,使用 useState
并設(shè)置初始值為 0
卦方,先完成了首次渲染。
然后俩檬,點(diǎn)擊頁面中的按鈕,觸發(fā) increment
中的 setState
對頁面進(jìn)行更新碾盟。
精簡版的業(yè)務(wù)邏輯如下棚辽,
標(biāo)紅的是首次渲染與更新不同的邏輯,留意以下幾個函數(shù)冰肴,
(1)首次渲染:mountIndeterminateComponent
屈藐,mountState
,createFiberFromElement
(2)更新:setState
熙尉,updateFunctionComponent
联逻,updateState
,useFiber
首次渲染
由 ReactDOM.render
觸發(fā)检痰,調(diào)用了組件的裝載方法 mountIndeterminateComponent
包归,
接著就執(zhí)行組件(函數(shù) App
)內(nèi)部的邏輯了。
App
內(nèi)部第一行就是 useState
铅歼,
它會創(chuàng)建了一個 hook
公壤,然后把它掛載到 fiber
上,
每個 hook
還包含了一個 queue
椎椰。
fiber
hook
quque
的關(guān)系如下厦幅,
fiber: { // Fiber
// 節(jié)點(diǎn)相關(guān)
sibling, child, return, index,
// 狀態(tài)相關(guān)
memoizedState: { // Hook
memorizedState, baseState, baseQueue, next,
queue: { // UpdateQueue
pending, dispatch, lastRenderedReducer, lastRenderedState,
},
},
updateQueue,
// 渲染相關(guān)
effectTag, nextEffect, alternate,
}
fiber
構(gòu)成了一棵樹,通過 sibling, child, return, index
相連俭识。
多個 hook
構(gòu)成一個循環(huán)鏈表慨削,通過 next
相連。
建好這些數(shù)據(jù)結(jié)構(gòu)之后套媚,useState
就返回了缚态,
return [hook.memoizedState, dispatch];
它返回了兩個值,一個是 hook.memoizedState
表示 hook
的當(dāng)前狀態(tài)堤瘤,
在首次渲染時玫芦,這個狀態(tài)就是初始值 0
。
另一個是 dispatch
函數(shù)本辐,也就是后文要用的 setState
桥帆。
useState
及外層的 App
組件函數(shù)執(zhí)行完后,
mountIndeterminateComponent
會接著調(diào)用 reconcileChildren
創(chuàng)建新的 fiber
慎皱。
fiber
整棵樹都更新完之后老虫,
就執(zhí)行 commitRoot
一次性的渲染到頁面上。
更新
更新邏輯在渲染到頁面之前茫多,分為兩步
(1)響應(yīng)事件
(2)更新狀態(tài)
(1)響應(yīng)事件
點(diǎn)擊頁面上的按鈕祈匙,React 會響應(yīng)綁定到 button
上的 onClick
事件,
從而調(diào)用了 increment
回調(diào)函數(shù)。
在這個 increment
函數(shù)中夺欲,我們的示例調(diào)用了兩次 setState
跪帝,
用于說明這個兩個 setState
的處理邏輯其實(shí)是不同的。
每個 setState
都會重新創(chuàng)建一個 update
對象些阅,
update
構(gòu)成了一個循環(huán)鏈表伞剑,用于記錄所有待更新的操作,結(jié)構(gòu)如下市埋,
update: {
expirationTime, suspenseConfig, action,
eagerReducer, eagerState,
next, priority,
}
雖然 update
對象都會創(chuàng)建黎泣,但卻只有第一個 setState
的 update
會被執(zhí)行。
剩下 setState
的 update
會記錄在鏈表中腰素。
(2)更新狀態(tài)
update
鏈表中的后續(xù)操作聘裁,會在下次調(diào)用 useState
的時候執(zhí)行。
仔細(xì)來看的話弓千,React 響應(yīng)完事件后衡便,會進(jìn)入 workLoopSync
循環(huán),
然后調(diào)用了 updateFunctionComponent
洋访,更新組件狀態(tài)镣陕。
此時會再次調(diào)用 App
函數(shù),useState
也被再次調(diào)用姻政。
然后 useState
執(zhí)行了和首次渲染不同的邏輯呆抑,調(diào)用了 updateState
,
執(zhí)行 update
循環(huán)鏈表中的所有的操作汁展,
最后把更新的結(jié)果返回鹊碍。
return [hook.memoizedState, dispatch];
此時 hook.memoizedState
是所有 setState
都執(zhí)行后的結(jié)果了。
Reconcile & Commit
剩下的流程就跟首次渲染大同小異了食绿,
先是調(diào)用 reconcileChildren
更新 fiber
樹侈咕,然后 commitRoot
渲染到頁面上。
總結(jié)
本文簡單梳理了 react@16.13.1 useState
和 setState
的業(yè)務(wù)邏輯器紧,
可以看到 hook 的主要神秘之處在于耀销,將 update
暫存起來異步更新囤攀。
以上分析還只是 React 源碼的九牛之一毛澄干,還需要不斷的學(xué)習(xí)探索。