以下文章來源于公眾號(hào):魔術(shù)師卡頌 ,作者卡頌
作為 React 開發(fā)者,你能答上如下兩個(gè)問題么:
- 對(duì)于如下函數(shù)組件:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
調(diào)用window.updateNum(1)
可以將視圖中的0
更新為1
么?
- 對(duì)于如下函數(shù)組件:
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return <p onClick={increment}>{num}</p>;
}
在1秒內(nèi)快速點(diǎn)擊p
5次,視圖上顯示為幾?
1. 可以
2. 顯示為1
其實(shí),這兩個(gè)問題本質(zhì)上是在問:
useState
如何保存狀態(tài)?useState
如何更新狀態(tài)?
本文會(huì)結(jié)合源碼,講透如上兩個(gè)問題。
這些,就是你需要了解的關(guān)于useState
的一切。
hook如何保存數(shù)據(jù)
FunctionComponent
的render
本身只是函數(shù)調(diào)用。
那么在render
內(nèi)部調(diào)用的hook
是如何獲取到對(duì)應(yīng)數(shù)據(jù)呢?
比如:
useState
獲取state
useRef
獲取ref
useMemo
獲取緩存的數(shù)據(jù)
答案是:
每個(gè)組件有個(gè)對(duì)應(yīng)的fiber節(jié)點(diǎn)
(可以理解為虛擬DOM
),用于保存組件相關(guān)信息。
每次FunctionComponent
render
時(shí),全局變量currentlyRenderingFiber
都會(huì)被賦值為該FunctionComponent
對(duì)應(yīng)的fiber節(jié)點(diǎn)
。
所以,hook
內(nèi)部其實(shí)是從currentlyRenderingFiber
中獲取狀態(tài)信息的。
多個(gè)hook如何獲取數(shù)據(jù)
我們知道,一個(gè)FunctionComponent
中可能存在多個(gè)hook
,比如:
function App() {
// hookA
const [a, updateA] = useState(0);
// hookB
const [b, updateB] = useState(0);
// hookC
const ref = useRef(0);
return <p></p>;
}
那么多個(gè)hook
如何獲取自己的數(shù)據(jù)呢?
答案是:
currentlyRenderingFiber.memoizedState
中保存一條hook
對(duì)應(yīng)數(shù)據(jù)的單向鏈表。
對(duì)于如上例子,可以理解為:
const hookA = {
// hook保存的數(shù)據(jù)
memoizedState: null,
// 指向下一個(gè)hook
next: hookB
// ...省略其他字段
};
hookB.next = hookC;
currentlyRenderingFiber.memoizedState = hookA;
當(dāng)FunctionComponent
render
時(shí),每執(zhí)行到一個(gè)hook
,都會(huì)將指向currentlyRenderingFiber.memoizedState
鏈表的指針向后移動(dòng)一次,指向當(dāng)前hook
對(duì)應(yīng)數(shù)據(jù)。
這也是為什么React
要求hook
的調(diào)用順序不能改變(不能在條件語句中使用hook
) —— 每次render
時(shí)都是從一條固定順序的鏈表中獲取hook
對(duì)應(yīng)數(shù)據(jù)的。
useState執(zhí)行流程
我們知道,useState
返回值數(shù)組第二個(gè)參數(shù)為改變state的方法。
在源碼中,他被稱為dispatchAction
。
每當(dāng)調(diào)用dispatchAction
,都會(huì)創(chuàng)建一個(gè)代表一次更新的對(duì)象update
:
const update = {
// 更新的數(shù)據(jù)
action: action,
// 指向下一個(gè)更新
next: null
};
對(duì)于如下例子
function App() {
const [num, updateNum] = useState(0);
function increment() {
updateNum(num + 1);
}
return <p onClick={increment}>{num}</p>;
}
調(diào)用updateNum(num + 1)
,會(huì)創(chuàng)建:
const update = {
// 更新的數(shù)據(jù)
action: 1,
// 指向下一個(gè)更新
next: null
// ...省略其他字段
};
如果是多次調(diào)用dispatchAction
,例如:
function increment() {
// 產(chǎn)生update1
updateNum(num + 1);
// 產(chǎn)生update2
updateNum(num + 2);
// 產(chǎn)生update3
updateNum(num + 3);
}
那么,update
會(huì)形成一條環(huán)狀鏈表。
update3 --next--> update1
^ |
| update2
|______next_______|
這條鏈表保存在哪里呢?
既然這條update
鏈表是由某個(gè)useState
的dispatchAction
產(chǎn)生,那么這條鏈表顯然屬于該useState hook
。
我們繼續(xù)補(bǔ)充hook
的數(shù)據(jù)結(jié)構(gòu)。
const hook = {
// hook保存的數(shù)據(jù)
memoizedState: null,
// 指向下一個(gè)hook
next: hookForB
// 本次更新以baseState為基礎(chǔ)計(jì)算新的state
baseState: null,
// 本次更新開始時(shí)已有的update隊(duì)列
baseQueue: null,
// 本次更新需要增加的update隊(duì)列
queue: null,
};
其中,queue
中保存了本次更新update
的鏈表。
在計(jì)算state
時(shí),會(huì)將queue
的環(huán)狀鏈表剪開掛載在baseQueue
最后面,baseQueue
基于baseState
計(jì)算新的state
。
在計(jì)算state
完成后,新的state
會(huì)成為memoizedState
。
為什么更新不基于
memoizedState
而是baseState
,是因?yàn)?code>state的計(jì)算過程需要考慮優(yōu)先級(jí),可能有些update
優(yōu)先級(jí)不夠被跳過。所以memoizedState
并不一定和baseState
相同。
回到我們開篇第一個(gè)問題:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
調(diào)用window.updateNum(1)
可以將視圖中的0
更新為1
么?
我們需要看看這里的updateNum
方法的具體實(shí)現(xiàn):
updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);
可見,updateNum
方法即綁定了currentlyRenderingFiber
與queue
(即hook.queue
)的dispatchAction
。
上文已經(jīng)介紹,調(diào)用dispatchAction
的目的是生成update
,并插入到hook.queue
鏈表中。
既然queue
作為預(yù)置參數(shù)已經(jīng)綁定給dispatchAction
,那么調(diào)用dispatchAction
就步僅局限在FunctionComponent
內(nèi)部了。
update的action
第二個(gè)問題
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return <p onClick={increment}>{num}</p>;
}
在1秒內(nèi)快速點(diǎn)擊p
5次,視圖上顯示為幾?
我們知道,調(diào)用updateNum
會(huì)產(chǎn)生update
,其中傳參會(huì)成為update.action
。
在1秒內(nèi)點(diǎn)擊5次。在點(diǎn)擊第五次時(shí),第一次點(diǎn)擊創(chuàng)建的update
還沒進(jìn)入更新流程,所以hook.baseState
還未改變。
那么這5次點(diǎn)擊產(chǎn)生的update
都是基于同一個(gè)baseState
計(jì)算新的state
,并且num
變量也還未變化(即5次update.action
(即num + 1
)為同一個(gè)值)。
所以,最終渲染的結(jié)果為1。
useState與useReducer
那么,如何5次點(diǎn)擊讓視圖從1逐步變?yōu)?呢?
由以上知識(shí)我們知道,需要改變baseState
或者action
。
其中baseState
由 React 的更新流程決定,我們無法控制。
但是我們可以控制action
。
action
不僅可以傳值
,也可以傳函數(shù)
。
// action為值
updateNum(num + 1);
// action為函數(shù)
updateNum(num => num + 1);
在基于baseState
與update
鏈表生成新state
的過程中:
let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;
// 遍歷baseQueue中的每一個(gè)update
do {
if (typeof update.action === 'function') {
newState = update.action(newState);
} else {
newState = action;
}
} while (update !== firstUpdate)
可見,當(dāng)傳值
時(shí),由于我們5次action
為同一個(gè)值,所以最終計(jì)算的newState
也為同一個(gè)值。
而傳函數(shù)
時(shí),newState
基于action
函數(shù)計(jì)算5次,則最終得到累加的結(jié)果。
如果這個(gè)例子中,我們使用useReducer
而不是useState
,由于useReducer
的action
始終為函數(shù)
,所以不會(huì)遇到我們例子中的問題。
事實(shí)上,useState
本身就是預(yù)置了如下reducer
的useReducer
。
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
總結(jié)
通過本文,我們了解了useState
的完整執(zhí)行過程。
以上就是W3Cschool編程獅
關(guān)于關(guān)于 useState 的一切的相關(guān)介紹了,希望對(duì)大家有所幫助。