網站首頁 編程語言 正文
熱身準備
在正式講useState
,我們先熱熱身,了解下必備知識。
為什么會有hooks
大家都知道hooks
是在函數組件的產物。之前class
組件為什么沒有出現hooks
這種東西呢?
答案很簡單,不需要。
因為在class
組件中,在運行時,只會生成一個實例,而在這個實例中會保存組件的state
等信息。在后續的更新操作中,也只是調用其中的render
方法,實例中的信息不會丟失。而在函數組件中,每次渲染,更新都會去執行這個函數組件,所以在函數組件中是沒辦法保存state
等信息的。為了保存state
等信息,于是有了hooks
,用來記錄函數組件的狀態,執行副作用。
hooks執行時機
上面提到,在函數組件中,每次渲染,更新都會去執行這個函數組件。所以我們在函數組件內部聲明的hooks
也會在每次執行函數組件時執行。
在這個時候,可能有的同學聽了我上面的說法(hooks
用來記錄函數組件的狀態,執行副作用),又有疑惑了,既然每次函數組件執行都會執行hooks
方法,那hooks
是怎么記錄函數組件的狀態的呢?
答案是,記錄在函數組件對應的fiber
節點中。
兩套hooks
在我們剛開始學習使用hooks
時,可能會有疑惑, 為什么hooks
要在函數組件的頂部聲明,而不能在條件語句或內部函數中聲明?
答案是,React
維護了兩套hooks
,一套用來在項目初始化mount
時,初始化hooks
。而在后續的更新操作中會基于初始化的hooks
執行更新操作。如果我們在條件語句或函數中聲明hooks
,有可能在項目初始化時不會聲明,這樣就會導致在后面的更新操作中出問題。
hooks存儲
提前講一下hooks存儲方式,避免看暈了~~~
每個初始化的hook
都會創建一個hook
結構,多個hook
是通過聲明順序用鏈表的結構相關聯,最終這個鏈表會存放在fiber.memoizedState
中:
var hook = { memoizedState: null, // 存儲hook操作,不要和fiber.memoizedState搞混了 baseState: null, baseQueue: null, queue: null, // 存儲該hook本次更新階段的所有更新操作 next: null // 鏈接下一個hook };
而在每個hook.queue
中存放的么個update
也是一個鏈表結構存儲的,千萬不要和hook
的鏈表搞混了。
接下來,讓我們帶著下面幾個問題看文章:
- 為什么
setState
后不能馬上拿到最新的state
的值? - 多個
setState
是如何合并的? -
setState
到底是同步還是異步的? - 為什么
setState
的值相同時,函數組件不更新?
假如我們有下面這樣一段代碼:
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()
函數:
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
上面的dispatcher
就會涉及到開始提到的兩套hooks
的變換使用,initialState
是我們傳入useState
的參數,可以是基礎數據類型,也可以是函數,我們主要看dispatcher.useState(initialState)
方法,因為我們這里是初始化,它會調用mountState
方法:相關參考視頻:傳送門
function mountState(initialState) { var hook = mountWorkInProgressHook(); // workInProgressHook if (typeof initialState === 'function') { // 在這里,如果我們傳入的參數是函數,會執行拿到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]; }
上面的代碼還是比較簡單,主要就是根據useState()
的入參生成一個queue
并保存在hook
中,然后將入參和綁定了兩個參數的dispatchAction
作為返回值暴露到函數組件中去使用。
這兩個返回值,第一個hook.memoizedState
比較好理解,就是初始值,第二個dispatch
,也就是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
這是個什么東西呢?
我們知道使用useState()
方法會返回兩個值state, setState
,這個setState
就對應上面的dispatchAction
,這個函數是怎么做到幫我們設置state
的值的呢?
我們先保留這個疑問,往下看,在后面會慢慢揭曉答案。
接下來我們主要看看mountWorkInProgressHook
都做了些什么。
mountWorkInProgressHook
function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; // 這里的if/else主要用來區分是否是第一個hook if (workInProgressHook === null) { currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; } else { // 把hook加到hooks鏈表的最后一條, 并且指針指向這條hook workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
從上面的currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
這一行代碼,我們可以發現,hook是存放在對應fiber.memoizedState
上的。
workInProgressHook = workInProgressHook.next = hook;
,從這一行代碼,我們能知道,如果是有多個hook
,他們是以鏈表的形式進行的存放。
不僅僅是useState()
這個hook
會在初始化時走mountWorkInProgressHook
方法,其他的hook
,例如:useEffect, useRef, useCallback
等在初始化時都是調用的這個方法。
到這里我們能搞明白兩件事:
-
hooks
的狀態數據是存放在對應的函數組件的fiber.memoizedState
; - 一個函數組件上如果有多個
hook
,他們會通過聲明的順序以鏈表的結構存儲;
到這里,我們的useState()
已經完成了它初始化時的所有工作了,簡單概括下,useState()
在初始化時會將我們傳入的初始值以hook
的結構存放到對應的fiber.memoizedState
,以數組形式返回[state, dispatchAction]
。
更新update
當我們以某種形式觸發setState()
時,React
也會根據setState()
的值來決定如何更新視圖。
在上面講到,useState
在初始化時會返回[state, dispatchAction]
,那我們調用setState()
方法,實際上就是調用dispatchAction
,而且這個函數在初始化時還通過bind
綁定了兩個參數, 一個是useState
初始化時函數組件對應的fiber
,另一個是hook
結構的queue
。
來看下我精簡后的dispatchAction
(去除了和setState
無關的代碼)
function dispatchAction(fiber, queue, action) { // 創建一個update,用于后續的更新,這里的action就是我們setState的入參 var update = { lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; // 這段閉環鏈表插入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語句里的一大段就是用來判斷我們這次更新是否和上次一樣,如果一樣就不會在進行調度更新 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進行調度更新 scheduleUpdateOnFiber(fiber, lane, eventTime); } }
上面的代碼已經是我盡力精簡的結果了。。。代碼上有注釋,各位看官湊合看下。
不愿細看的我來總結下dispatchAction
做的事情:
- 創建一個
update
并加入到fiber.hook.queue
鏈表中,并且鏈表指針指向這個update
; - 判斷當前是否是渲染階段決定要不要馬上調度更新;
- 判斷這次的操作和上次的操作是否相同, 如果相同則不進行調度更新;
- 滿足上述條件則將帶有
update
的fiber
進行調度更新;
到這里我們又搞明白了一個問題:
為什么setState
的值相同時,函數組件不更新?
updateState
我們這里不詳細講解調度更新的過程, 后面文章安排, 這里我們只需要知道,在接下來更新過程中,會再次執行我們的函數組件,這時又會調用useState
方法了。前面講過,React維護了兩套hooks
,一套用于初始化, 一套用于更新。 這個在調度更新時就已經完成了切換。所以我們這次調用useState
方法會和之前初始化有所不同。
這次我們進入useState
,會看到其實是調用的updateState
方法
function updateState(initialState) { return updateReducer(basicStateReducer); }
看到這幾行代碼,看官們應該就明白為什么網上有人說useState
和useReducer
相似。原來在useState
的更新中調用的就是updateReducer
啊。
updateReducer
本來很長,想讓各位看官忍一忍。于心不忍,忍痛減了很多
function updateReducer(reducer, initialArg, init) { // 創建一個新的hook,帶有dispatchAction創建的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) { // 從這里能看到之前講的創建閉環鏈表插入update的好處了吧?直接next就能找到第一個update var first = baseQueue.next; var newState = current.baseState; var update = first; // 開始遍歷update鏈表執行所有setState do { var updateLane = update.lane; // 假如我們這個update上有多個setState,在循環過程中,最終都會做合并操作 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]; }
上面的更新中,會循環遍歷update
進行一個合并操作,只取最后一個setState
的值,這時候可能有人會問那直接取最后一個setState
的值不是更方便嗎?
這樣做是不行的,因為setState
入參可以是基礎類型也可以是函數, 如果傳入的是函數,它會依賴上一個setState
的值來完成更新操作,下面的代碼就是上面的循環中的reducer
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
到這里我們搞明白了一個問題,多個setState
是如何合并的?
updateWorkInProgressHook
下面是偽代碼,我把很多的邏輯判斷給刪除了,免了太長又讓各位看官難受,原來的代碼里會判斷當前的hook
是不是第一個調度更新的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
創建一個新的hook
結構覆蓋之前的hook
。前面dispatchAction
講到會把update
加入到hook.queue
中,在這里的newHook.queue
上就有這個update
。
總結
總結下useState
初始化和setState
更新:
-
useState
會在第一次執行函數組件時進行初始化,返回[state, dispatchAction]
。 - 當我們通過
setState
也就是dispatchAction
進行調度更新時,會創建一個update
加入到hook.queue
中。 - 當更新過程中再次執行函數組件,也會調用
useState
方法,此時的useState
內部會使用更新時的hooks
。 - 通過
updateWorkInProgressHook
獲取到dispatchAction
創建的update
。 - 在
updateReducer
通過遍歷update
鏈表完成setState
合并。 - 返回
update
后的[newState, dispatchAction]
.
還有兩個問題
為什么setState
后不能馬上拿到最新的state
的值? React
其實可以這么做,為什么沒有這么做,因為每個setState
都會觸發更新,React
出于性能考慮,會做一個合并操作。所以setState
只是觸發了dispatchAction
生成了一個update
的動作,新的state
會存儲在update
中,等到下一次render
, 觸發這個useState
所在的函數組件執行,才會賦值新的state
。
setState
到底是同步還是異步的?
同步的,假如我們有這樣一段代碼:
const handleClick = () => { setCount(2) setCount(count => count + 1) console.log('after setCount') }
你會驚奇的發現頁面還沒有更新count
,但是控制臺已經打印了after setCount
。
之所以表現上像是異步,是因為內部使用了try{...}finally{...}
。當調用setState
觸發調度更新時,更新操作會放在finally
中,返回去繼續執行handlelick
的邏輯。于是會出現上面的情況。
看完這篇文章, 我們可以弄明白下面這幾個問題:
- 為什么
setState
后不能馬上拿到最新的state
的值? - 多個
setState
是如何合并的? -
setState
到底是同步還是異步的? - 為什么
setState
的值相同時,函數組件不更新? -
setState
是怎么完成更新的? -
useState
是什么時候初始化又是什么時候開始更新的?
原文鏈接:https://blog.csdn.net/It_kc/article/details/127644784
相關推薦
- 2022-03-15 request doesn‘t contain a multipart/form-data or m
- 2022-05-18 一起來了解React的Hook_React
- 2022-03-18 C語言字符串函數操作(strlen,strcpy,strcat,strcmp)詳解_C 語言
- 2022-03-16 Redis在項目中的使用(JedisPool方式)_Redis
- 2022-06-28 python神經網絡Keras構建CNN網絡訓練_python
- 2022-09-09 python中對開區間和閉區間的理解_python
- 2022-09-02 SQL注入的四種防御方法總結_數據庫其它
- 2022-05-17 基于Pytorch的神經網絡之Regression的實現_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支