做爲React
開發者,你能答上以下兩個問題麼:html
-
對於以下函數組件:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
調用window.updateNum(1)
能夠將視圖中的0
更新爲1
麼?react
-
對於以下函數組件:
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return <p onClick={increment}>{num}</p>;
}
在1秒內快速點擊p
5次,視圖上顯示爲幾?web
👉向右滑動展現答案 1. 能夠
2. 顯示爲1
其實,這兩個問題本質上是在問:數組
-
useState
如何保存狀態?緩存 -
useState
如何更新狀態?微信
本文會結合源碼,講透如上兩個問題。數據結構
這些,就是你須要瞭解的關於useState
的一切。編輯器
hook如何保存數據
FunctionComponent
的render
自己只是函數調用。函數
那麼在render
內部調用的hook
是如何獲取到對應數據呢?flex
好比:
-
useState
獲取state
-
useRef
獲取ref
-
useMemo
獲取緩存的數據
答案是:
每一個組件有個對應的fiber節點
(能夠理解爲虛擬DOM
),用於保存組件相關信息。
每次FunctionComponent
render
時,全局變量currentlyRenderingFiber
都會被賦值爲該FunctionComponent
對應的fiber節點
。
因此,hook
內部實際上是從currentlyRenderingFiber
中獲取狀態信息的。
多個hook如何獲取數據
咱們知道,一個FunctionComponent
中可能存在多個hook
,好比:
function App() {
// hookA
const [a, updateA] = useState(0);
// hookB
const [b, updateB] = useState(0);
// hookC
const ref = useRef(0);
return <p></p>;
}
那麼多個hook
如何獲取本身的數據呢?
答案是:
currentlyRenderingFiber.memoizedState
中保存一條hook
對應數據的單向鏈表。
對於如上例子,能夠理解爲:
const hookA = {
// hook保存的數據
memoizedState: null,
// 指向下一個hook
next: hookB
// ...省略其餘字段
};
hookB.next = hookC;
currentlyRenderingFiber.memoizedState = hookA;
當FunctionComponent
render
時,每執行到一個hook
,都會將指向currentlyRenderingFiber.memoizedState
鏈表的指針向後移動一次,指向當前hook
對應數據。
這也是爲何React
要求hook
的調用順序不能改變(不能在條件語句中使用hook
) —— 每次render
時都是從一條固定順序的鏈表中獲取hook
對應數據的。
![](http://static.javashuo.com/static/loading.gif)
useState執行流程
咱們知道,useState
返回值數組第二個參數爲改變state的方法。
在源碼中,他被稱爲dispatchAction
。
每當調用dispatchAction
,都會建立一個表明一次更新的對象update
:
const update = {
// 更新的數據
action: action,
// 指向下一個更新
next: null
};
對於以下例子
function App() {
const [num, updateNum] = useState(0);
function increment() {
updateNum(num + 1);
}
return <p onClick={increment}>{num}</p>;
}
調用updateNum(num + 1)
,會建立:
const update = {
// 更新的數據
action: 1,
// 指向下一個更新
next: null
// ...省略其餘字段
};
若是是屢次調用dispatchAction
,例如:
function increment() {
// 產生update1
updateNum(num + 1);
// 產生update2
updateNum(num + 2);
// 產生update3
updateNum(num + 3);
}
那麼,update
會造成一條環狀鏈表。
update3 --next--> update1
^ |
| update2
|______next_______|
這條鏈表保存在哪裏呢?
既然這條update
鏈表是由某個useState
的dispatchAction
產生,那麼這條鏈表顯然屬於該useState hook
。
咱們繼續補充hook
的數據結構。
const hook = {
// hook保存的數據
memoizedState: null,
// 指向下一個hook
next: hookForB
// 本次更新以baseState爲基礎計算新的state
baseState: null,
// 本次更新開始時已有的update隊列
baseQueue: null,
// 本次更新須要增長的update隊列
queue: null,
};
其中,queue
中保存了本次更新update
的鏈表。
在計算state
時,會將queue
的環狀鏈表剪開掛載在baseQueue
最後面,baseQueue
基於baseState
計算新的state
。
在計算state
完成後,新的state
會成爲memoizedState
。
![](http://static.javashuo.com/static/loading.gif)
爲何更新不基於
memoizedState
而是baseState
,是由於state
的計算過程須要考慮優先級,可能有些update
優先級不夠被跳過。因此memoizedState
並不必定和baseState
相同。更詳細的解釋見React技術揭祕[1]
回到咱們開篇第一個問題:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
調用window.updateNum(1)
能夠將視圖中的0
更新爲1
麼?
咱們須要看看這裏的updateNum
方法的具體實現:
updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);
可見,updateNum
方法即綁定了currentlyRenderingFiber
與queue
(即hook.queue
)的dispatchAction
。
上文已經介紹,調用dispatchAction
的目的是生成update
,並插入到hook.queue
鏈表中。
既然queue
做爲預置參數已經綁定給dispatchAction
,那麼調用dispatchAction
就步僅侷限在FunctionComponent
內部了。
update的action
第二個問題
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return <p onClick={increment}>{num}</p>;
}
在1秒內快速點擊p
5次,視圖上顯示爲幾?
咱們知道,調用updateNum
會產生update
,其中傳參會成爲update.action
。
在1秒內點擊5次。在點擊第五次時,第一次點擊建立的update
還沒進入更新流程,因此hook.baseState
還未改變。
那麼這5次點擊產生的update
都是基於同一個baseState
計算新的state
,而且num
變量也還未變化(即5次update.action
(即num + 1
)爲同一個值)。
因此,最終渲染的結果爲1。
useState與useReducer
那麼,如何5次點擊讓視圖從1逐步變爲5呢?
由以上知識咱們知道,須要改變baseState
或者action
。
其中baseState
由React
的更新流程決定,咱們沒法控制。
可是咱們能夠控制action
。
action
不只能夠傳值
,也能夠傳函數
。
// action爲值
updateNum(num + 1);
// action爲函數
updateNum(num => num + 1);
在基於baseState
與update
鏈表生成新state
的過程當中:
let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;
// 遍歷baseQueue中的每個update
do {
if (typeof update.action === 'function') {
newState = update.action(newState);
} else {
newState = action;
}
} while (update !== firstUpdate)
可見,當傳值
時,因爲咱們5次action
爲同一個值,因此最終計算的newState
也爲同一個值。
而傳函數
時,newState
基於action
函數計算5次,則最終獲得累加的結果。
若是這個例子中,咱們使用useReducer
而不是useState
,因爲useReducer
的action
始終爲函數
,因此不會遇到咱們例子中的問題。
事實上,useState
自己就是預置了以下reducer
的useReducer
。
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
總結
經過本文,咱們瞭解了useState
的完整執行過程。
本系列文章接下來會繼續以實例
+ 源碼
的方式,解讀業務中常常使用的React
特性。
點擊
閱讀原文
,開源電子書輕鬆學懂React
源碼
參考資料
React技術揭祕: https://react.iamkasong.com/state/priority.html#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BC%98%E5%85%88%E7%BA%A7
本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。