在項目中,我們通常會使用useState來初始化並更新數據。如下:
function App(){
const [num, setNum] = useState(0);
function increment() {
setTimeout(() => {
setNum(num + 1);
}, 1000);
}
return <button onClick={increment}>{num}</button>;
}
num初始化為0,點擊按鈕進行加一操作。但是在以上代碼中,如果用戶在一秒內點擊五次按鈕,最后依然會顯示1。
為什么呢?這就不得不聊聊useState是如何工作的了。
hook如何保存數據
在react中通過currentRenderingFiber來標識當前渲染節點,每個組件都有一個對應的fiber節點,用來保存組件的相關數據信息。
每次函數組件渲染時,currentRenderingFiber就被賦值為當前組件對應的fiber,所以實際上hook是通過currentRenderingFiber來獲取狀態信息的。
多個hook如何獲取數據
react hook允許我們在一個組件中多次使用hook。如下:
function App(){
const [num, setNum] = useState(0);
const [name, setName] = useState('ashen');
const [age, setAge] = useState(21);
}
currentRenderingFiber.memorizedState中保存一條hook對應數據的單向鏈表。
const hookNum = {
memorizedState: null,
next: hookName
}
hookName.next = hookAge;
currentRenderingFiber.memorizedState.next = hookNum;
當函數組件渲染時,每執行到一個hook,就會將currentRenderingFiber.memorizedState的指針向后移一下。這也是hook的調用順序不能改變的原因(不能再條件語句中使用hook)
hook如何更新數據
使用useState時,返回值數組的第二個參數是用來更新數據的,稱為dispatchAction.
每當調用dispatchAction時,都會創建一個update對象:
const update = {
// 更新數據
action: action,
// 指向下一個更新
next: null
}
當我們多次更新state時,會形成一條環式更新鏈表
在以上例子中,如果點擊按鈕多次進行自增操作
function increment() {
// 產生update1
updateNum(num + 1);
// 產生update2
updateNum(num + 2);
// 產生update3
updateNum(num + 3);
}
update3 --next--> update1
^ |
| update2
|______next_______|
這些dispatchAction產生的update對象也會保存在hook當中
const hook = {
memorizedState: null,
next: null,
baseState: null,
baseQueue: null,
queue: null
}
其中queue中保存了本次更新的鏈表,baseQueue中保存了本次更新開始時已有的鏈表,在計算state時,會基於baseState進行更新操作。在計算state完成后,新的state就會成為memorizedState。
為什么更新不基於memoizedState而是baseState,是因為state的計算過程需要考慮優先級,可能有些update優先級不夠被跳過。所以memoizedState並不一定和baseState相同。
現在我們回到最開始的那個問題,在一秒內點擊五次按鈕進行更新,但是在這五次更新生成的時候,第一次的更新還沒有進行,所以baseState並未改變,都是基於0進行更新,點擊五次后依然是1。
那么如何解決這個問題呢?實際上,更新state的函數不只可以傳值,還可以傳函數。
function increment() {
setTimeout(() => {
setNum(num => num + 1);
}, 1000);
}
在基於baseState和update更新state時:
let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;
if(typeof update === 'function'){
newState = update.action(newState)
}else {
newState = action;
}
如上可見,當傳入值時,由於5次action都是同一個值,所以結果為1.
當傳入函數時,每次更新都是基於上一次更新后的值進行改變,所以點擊五次會變為5。
而上例中,如果使用的是useReducer,由於第二個參數action只能是函數,所以不會產生上述問題。
useState實際上就是內置了如下reducer的useReducer
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
可以用下圖來概括本文內容