React超詳細分析useState與useReducer源碼
熱身準備
在正式講useState
,我們先熱熱身,了解下必備知識。
為什么會有hooks
大家都知道hooks
是在函數(shù)組件的產(chǎn)物。之前class
組件為什么沒有出現(xiàn)hooks
這種東西呢?
答案很簡單,不需要。
因為在class
組件中,在運行時,只會生成一個實例,而在這個實例中會保存組件的state
等信息。在后續(xù)的更新操作中,也只是調(diào)用其中的render
方法,實例中的信息不會丟失。而在函數(shù)組件中,每次渲染,更新都會去執(zhí)行這個函數(shù)組件,所以在函數(shù)組件中是沒辦法保存state
等信息的。為了保存state
等信息,于是有了hooks
,用來記錄函數(shù)組件的狀態(tài),執(zhí)行副作用。
hooks執(zhí)行時機
上面提到,在函數(shù)組件中,每次渲染,更新都會去執(zhí)行這個函數(shù)組件。所以我們在函數(shù)組件內(nèi)部聲明的hooks
也會在每次執(zhí)行函數(shù)組件時執(zhí)行。
在這個時候,可能有的同學聽了我上面的說法(hooks
用來記錄函數(shù)組件的狀態(tài),執(zhí)行副作用),又有疑惑了,既然每次函數(shù)組件執(zhí)行都會執(zhí)行hooks
方法,那hooks
是怎么記錄函數(shù)組件的狀態(tài)的呢?
答案是,記錄在函數(shù)組件對應的fiber
節(jié)點中。
兩套hooks
在我們剛開始學習使用hooks
時,可能會有疑惑, 為什么hooks
要在函數(shù)組件的頂部聲明,而不能在條件語句或內(nèi)部函數(shù)中聲明?
答案是,React
維護了兩套hooks
,一套用來在項目初始化mount
時,初始化hooks
。而在后續(xù)的更新操作中會基于初始化的hooks
執(zhí)行更新操作。如果我們在條件語句或函數(shù)中聲明hooks
,有可能在項目初始化時不會聲明,這樣就會導致在后面的更新操作中出問題。
hooks存儲
提前講一下hooks存儲方式,避免看暈了~~~
每個初始化的hook
都會創(chuàng)建一個hook
結(jié)構(gòu),多個hook
是通過聲明順序用鏈表的結(jié)構(gòu)相關(guān)聯(lián),最終這個鏈表會存放在fiber.memoizedState
中:
var hook = { memoizedState: null, // 存儲hook操作,不要和fiber.memoizedState搞混了 baseState: null, baseQueue: null, queue: null, // 存儲該hook本次更新階段的所有更新操作 next: null // 鏈接下一個hook };
而在每個hook.queue
中存放的么個update
也是一個鏈表結(jié)構(gòu)存儲的,千萬不要和hook
的鏈表搞混了。
接下來,讓我們帶著下面幾個問題看文章:
- 為什么
setState
后不能馬上拿到最新的state
的值? - 多個
setState
是如何合并的? setState
到底是同步還是異步的?- 為什么
setState
的值相同時,函數(shù)組件不更新?
假如我們有下面這樣一段代碼:
function App(){ const [count, setCount] = useState(0) const handleClick = () => { setCount(count => count + 1) } return ( <div> 勇敢牛牛, <span>不怕困難</span> <span onClick={handleClick}>{count}</span> </div> ) }
初始化 mount
useState
我們先來看下useState()
函數(shù):
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
上面的dispatcher
就會涉及到開始提到的兩套hooks
的變換使用,initialState
是我們傳入useState
的參數(shù),可以是基礎數(shù)據(jù)類型,也可以是函數(shù),我們主要看dispatcher.useState(initialState)
方法,因為我們這里是初始化,它會調(diào)用mountState
方法:相關(guān)參考視頻:傳送門
function mountState(initialState) { var hook = mountWorkInProgressHook(); // workInProgressHook if (typeof initialState === 'function') { // 在這里,如果我們傳入的參數(shù)是函數(shù),會執(zhí)行拿到return作為initialState initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
上面的代碼還是比較簡單,主要就是根據(jù)useState()
的入?yún)⑸梢粋€queue
并保存在hook
中,然后將入?yún)⒑徒壎藘蓚€參數(shù)的dispatchAction
作為返回值暴露到函數(shù)組件中去使用。
這兩個返回值,第一個hook.memoizedState
比較好理解,就是初始值,第二個dispatch
,也就是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
這是個什么東西呢?
我們知道使用useState()
方法會返回兩個值state, setState
,這個setState
就對應上面的dispatchAction
,這個函數(shù)是怎么做到幫我們設置state
的值的呢?
我們先保留這個疑問,往下看,在后面會慢慢揭曉答案。
接下來我們主要看看mountWorkInProgressHook
都做了些什么。
mountWorkInProgressHook
function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; // 這里的if/else主要用來區(qū)分是否是第一個hook if (workInProgressHook === null) { currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; } else { // 把hook加到hooks鏈表的最后一條, 并且指針指向這條hook workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
從上面的currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
這一行代碼,我們可以發(fā)現(xiàn),hook是存放在對應fiber.memoizedState
上的。
workInProgressHook = workInProgressHook.next = hook;
,從這一行代碼,我們能知道,如果是有多個hook
,他們是以鏈表的形式進行的存放。
不僅僅是useState()
這個hook
會在初始化時走mountWorkInProgressHook
方法,其他的hook
,例如:useEffect, useRef, useCallback
等在初始化時都是調(diào)用的這個方法。
到這里我們能搞明白兩件事:
hooks
的狀態(tài)數(shù)據(jù)是存放在對應的函數(shù)組件的fiber.memoizedState
;- 一個函數(shù)組件上如果有多個
hook
,他們會通過聲明的順序以鏈表的結(jié)構(gòu)存儲;
到這里,我們的useState()
已經(jīng)完成了它初始化時的所有工作了,簡單概括下,useState()
在初始化時會將我們傳入的初始值以hook
的結(jié)構(gòu)存放到對應的fiber.memoizedState
,以數(shù)組形式返回[state, dispatchAction]
。
更新update
當我們以某種形式觸發(fā)setState()
時,React
也會根據(jù)setState()
的值來決定如何更新視圖。
在上面講到,useState
在初始化時會返回[state, dispatchAction]
,那我們調(diào)用setState()
方法,實際上就是調(diào)用dispatchAction
,而且這個函數(shù)在初始化時還通過bind
綁定了兩個參數(shù), 一個是useState
初始化時函數(shù)組件對應的fiber
,另一個是hook
結(jié)構(gòu)的queue
。
來看下我精簡后的dispatchAction
(去除了和setState
無關(guān)的代碼)
function dispatchAction(fiber, queue, action) { // 創(chuàng)建一個update,用于后續(xù)的更新,這里的action就是我們setState的入?yún)? var update = { lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; // 這段閉環(huán)鏈表插入update的操作有沒有很熟悉? var pending = queue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; var alternate = fiber.alternate; // 判斷當前是否是渲染階段 if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { var lastRenderedReducer = queue.lastRenderedReducer; // 這個if語句里的一大段就是用來判斷我們這次更新是否和上次一樣,如果一樣就不會在進行調(diào)度更新 if (lastRenderedReducer !== null) { var prevDispatcher; { prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { return; } } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; } } } } // 將攜帶有update的fiber進行調(diào)度更新 scheduleUpdateOnFiber(fiber, lane, eventTime); } }
上面的代碼已經(jīng)是我盡力精簡的結(jié)果了。。。代碼上有注釋,各位看官湊合看下。
不愿細看的我來總結(jié)下dispatchAction
做的事情:
- 創(chuàng)建一個
update
并加入到fiber.hook.queue
鏈表中,并且鏈表指針指向這個update
; - 判斷當前是否是渲染階段決定要不要馬上調(diào)度更新;
- 判斷這次的操作和上次的操作是否相同, 如果相同則不進行調(diào)度更新;
- 滿足上述條件則將帶有
update
的fiber
進行調(diào)度更新;
到這里我們又搞明白了一個問題:
為什么setState
的值相同時,函數(shù)組件不更新?
updateState
我們這里不詳細講解調(diào)度更新的過程, 后面文章安排, 這里我們只需要知道,在接下來更新過程中,會再次執(zhí)行我們的函數(shù)組件,這時又會調(diào)用useState
方法了。前面講過,React維護了兩套hooks
,一套用于初始化, 一套用于更新。 這個在調(diào)度更新時就已經(jīng)完成了切換。所以我們這次調(diào)用useState
方法會和之前初始化有所不同。
這次我們進入useState
,會看到其實是調(diào)用的updateState
方法
function updateState(initialState) { return updateReducer(basicStateReducer); }
看到這幾行代碼,看官們應該就明白為什么網(wǎng)上有人說useState
和useReducer
相似。原來在useState
的更新中調(diào)用的就是updateReducer
啊。
updateReducer
本來很長,想讓各位看官忍一忍。于心不忍,忍痛減了很多
function updateReducer(reducer, initialArg, init) { // 創(chuàng)建一個新的hook,帶有dispatchAction創(chuàng)建的update var hook = updateWorkInProgressHook(); var queue = hook.queue; queue.lastRenderedReducer = reducer; var current = currentHook; var baseQueue = current.baseQueue; var pendingQueue = queue.pending; current.baseQueue = baseQueue = pendingQueue; if (baseQueue !== null) { // 從這里能看到之前講的創(chuàng)建閉環(huán)鏈表插入update的好處了吧?直接next就能找到第一個update var first = baseQueue.next; var newState = current.baseState; var update = first; // 開始遍歷update鏈表執(zhí)行所有setState do { var updateLane = update.lane; // 假如我們這個update上有多個setState,在循環(huán)過程中,最終都會做合并操作 var action = update.action; // 這里的reducer會判斷action類型,下面講 newState = reducer(newState, action); update = update.next; } while (update !== null && update !== first); hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } var dispatch = queue.dispatch; return [hook.memoizedState, dispatch]; }
上面的更新中,會循環(huán)遍歷update
進行一個合并操作,只取最后一個setState
的值,這時候可能有人會問那直接取最后一個setState
的值不是更方便嗎?
這樣做是不行的,因為setState
入?yún)⒖梢允腔A類型也可以是函數(shù), 如果傳入的是函數(shù),它會依賴上一個setState
的值來完成更新操作,下面的代碼就是上面的循環(huán)中的reducer
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
到這里我們搞明白了一個問題,多個setState
是如何合并的?
updateWorkInProgressHook
下面是偽代碼,我把很多的邏輯判斷給刪除了,免了太長又讓各位看官難受,原來的代碼里會判斷當前的hook
是不是第一個調(diào)度更新的hook
,我這里為了簡單就按第一個來解析
function updateWorkInProgressHook() { var nextCurrentHook; nextCurrentHook = current.memoizedState; var newHook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null } currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook; return workInProgressHook; }
從上面代碼能看出來,updateWorkInProgressHook
拋去那些判斷, 其實做的事情也很簡單,就是基于fiber.memoizedState
創(chuàng)建一個新的hook
結(jié)構(gòu)覆蓋之前的hook
。前面dispatchAction
講到會把update
加入到hook.queue
中,在這里的newHook.queue
上就有這個update
。
總結(jié)
總結(jié)下useState
初始化和setState
更新:
useState
會在第一次執(zhí)行函數(shù)組件時進行初始化,返回[state, dispatchAction]
。- 當我們通過
setState
也就是dispatchAction
進行調(diào)度更新時,會創(chuàng)建一個update
加入到hook.queue
中。 - 當更新過程中再次執(zhí)行函數(shù)組件,也會調(diào)用
useState
方法,此時的useState
內(nèi)部會使用更新時的hooks
。 - 通過
updateWorkInProgressHook
獲取到dispatchAction
創(chuàng)建的update
。 - 在
updateReducer
通過遍歷update
鏈表完成setState
合并。 - 返回
update
后的[newState, dispatchAction]
.
還有兩個問題
為什么setState
后不能馬上拿到最新的state
的值? React
其實可以這么做,為什么沒有這么做,因為每個setState
都會觸發(fā)更新,React
出于性能考慮,會做一個合并操作。所以setState
只是觸發(fā)了dispatchAction
生成了一個update
的動作,新的state
會存儲在update
中,等到下一次render
, 觸發(fā)這個useState
所在的函數(shù)組件執(zhí)行,才會賦值新的state
。
setState
到底是同步還是異步的?
同步的,假如我們有這樣一段代碼:
const handleClick = () => { setCount(2) setCount(count => count + 1) console.log('after setCount') }
你會驚奇的發(fā)現(xiàn)頁面還沒有更新count
,但是控制臺已經(jīng)打印了after setCount
。
之所以表現(xiàn)上像是異步,是因為內(nèi)部使用了try{...}finally{...}
。當調(diào)用setState
觸發(fā)調(diào)度更新時,更新操作會放在finally
中,返回去繼續(xù)執(zhí)行handlelick
的邏輯。于是會出現(xiàn)上面的情況。
看完這篇文章, 我們可以弄明白下面這幾個問題:
- 為什么
setState
后不能馬上拿到最新的state
的值? - 多個
setState
是如何合并的? setState
到底是同步還是異步的?- 為什么
setState
的值相同時,函數(shù)組件不更新? setState
是怎么完成更新的?useState
是什么時候初始化又是什么時候開始更新的?
到此這篇關(guān)于React超詳細分析useState與useReducer源碼的文章就介紹到這了,更多相關(guān)React useState與useReducer內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
40行代碼把Vue3的響應式集成進React做狀態(tài)管理
這篇文章主要介紹了40行代碼把Vue3的響應式集成進React做狀態(tài)管理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-05-05React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案
這篇文章主要為大家介紹了React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02