1. 回顧
上一篇 文章我們介紹了 React 函數(shù)組件的更新過程。
我們來總結以下 組件加載 和 更新 的全流程。
(1)組件載入時,會創(chuàng)建兩棵 Fiber Tree
一棵為當前已寫入 DOM 的 Fiber Tree(名為 current)。
在 commit 階段 之前這個 Fiber Tree 只有一個根節(jié)點。
另一棵為當前正在渲染的 Fiber Tree,(名為 workInProgress)。
render 階段 就是在創(chuàng)建它。
到了 commit 階段,React 會將 workInProgress 的 Fiber Tree 實際寫到 DOM 中,
然后將 current 指向這個 Fiber Tree。
這樣就完成了組件的首次加載。
(2)事件觸發(fā)組件更新時
首先是由 React 的事件系統(tǒng)監(jiān)聽到用戶事件,然后觸發(fā)用戶綁定的事件處理函數(shù)。
在這個事件處理函數(shù)中,示例中我們用了 hook setState 來更新組件組件狀態(tài)。
執(zhí)行過程中,會將 performSyncWorkOnRoot 放到 syncQueue 中。
然后,用戶事件就執(zhí)行完了。
用戶事件執(zhí)行完之后,React 會緊接著執(zhí)行 flushSyncCallbackQueue,
獲取到 syncQueue 中的 performSyncWorkOnRoot 進行執(zhí)行。
performSyncWorkOnRoot 實際上就是組件的 render 和 commit 方法。
(在組件的第一次更新時)它會創(chuàng)建一棵 workInProgress 的 Fiber Tree,然后在 commit 階段 寫到 DOM 中(之后,將 current 指向這棵 Fiber Tree)。
(如果是組件非首次更新,此時內存中已經(jīng)有了兩棵 Fiber Tree 了,此時 render 階段,并不會重新創(chuàng)建一棵全新的 Fiber Tree,而是盡可能利用現(xiàn)有 Fiber Tree 的節(jié)點,這個邏輯在 createWorkInProgress 中控制)。
如此這般,就完成了組件的更新。
以上分析中,我們是從 Fiber Tree 的角度,從 render 和 commit 的角度來看待組件的更新過程,
略過了組件的狀態(tài)的計算過程。
在實際開發(fā)中,常見的場景是,
- 有多個 hook(
setState) - 一次更新調用了多次
setState
React 內部是如何處理這個狀態(tài)計算的呢?本文我們來仔細研究下這個問題。
2. 場景:多個 hook
2.1 示例項目的修改
參考 example-project/src/AppTwoState.js
我們修改了 App 組件如下,
const App = () => {
debugger;
const [state1, setState1] = useState(0);
debugger;
const [state2, setState2] = useState('a');
debugger;
const onDivClick = () => {
debugger;
setState1(1);
debugger;
setState2('b');
debugger;
};
debugger;
return <div onClick={onDivClick}>
{state1}-{state2}
</div>;
}
其中用到了兩個 hook(都是 useState),這樣會給 App 組件創(chuàng)建兩個獨立的狀態(tài) state1 state2。
2.2 兩個 hook 的更新流程
我們來跟蹤一下兩個 useState 和 setState1 setState2 的執(zhí)行過程。
完整的執(zhí)行流程在這里:7.1 hook 原理:多個 hook,總共分為三個部分:
-
組件首次加載時,調用
useState(第 1-50 行)
-
用戶點擊 div 時,
setState調用lastRenderedReducer更新狀態(tài)(第 51-96 行)
-
事件響應完之前,React 調用
flushSyncCallbackQueue更新狀態(tài)(第 97-154 行)
2.3 多個 hook 是怎么存儲的
我們看到組件載入的時候,useState 會調用 mountWorkInProgressHook

function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; // 第一個 useState
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook; // 第二個 useState
}
return workInProgressHook;
}
每次調用 useState 會創(chuàng)建一個新的 hook,多個 hook 構成了一個鏈表結構(第二個 hook 的 next 指向 第一個 hook)
(1)第一個 hook

currentlyRenderingFiber$1 為 <App /> 節(jié)點(Fiber Node),并且,F(xiàn)iber Node 的
memorizedState 指向了 hook 鏈表的第一個 hook。
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
(2)第二個 hook
設置 第一個 hook 的 next 屬性指向 第二個 hook,

通過 Fiber Node(currentlyRenderingFiber$1)觀察一下 hook 鏈表的結構,
currentlyRenderingFiber$1.memorizedState -> hook1
hook1.next -> hook2
hook2.next -> null

2.4 dispatch(setState1 setState2)
雖然 hook 是通過鏈表結構來存儲的,但實際調用 setState1 setState2 的時候,卻并不是通過鏈表來取的。
這是是因為雖然 setState 只傳入了一個參數(shù) action,

但實際 React 已通過 bind 傳入了其他參數(shù),另外兩個參數(shù)是 fiber 和 queue,

fiber 就是上文那個 currentlyRenderingFiber$1,queue 就是 setState1 對應 hook 的 queue 屬性值(hook 相關的 update quque,下文介紹)

所以調用 setState1 setState2 時不用在 hook 鏈表中進行查找,而是直接進入 dispatchAction 函數(shù)中。
3. 場景:多次 dispatch
上文介紹了多個 hook 的存儲和調用原理,在實際項目中,還會有一個事件中多次調用了 dispatch(setState),
這些 dispatch 函數(shù)也許是同一個狀態(tài)的 dispatch(多次調用 setState),也許是不同狀態(tài)的(先后調用 setState1 setState2)。
原理其實是大同小異的,為了簡單起見,本文只介紹后者,即,一個事件中,多次調用了同一個 hook 的 dispatch(setState)的執(zhí)行流程。
3.1 示例項目的修改
示例項目的修改如下,example-project/src/AppAsyncState.js
(為了便于跟蹤,setState 采用了回調方式進行編寫)
const App = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(s => {
debugger;
return s + 1;
});
debugger;
setState(s => {
debugger;
return s + 2;
});
debugger;
};
debugger;
return <div onClick={onDivClick}>
{state}
</div>;
}
3.2 多次調用 setState 的執(zhí)行流程
完整的執(zhí)行流程可參考 7.2 hook 原理:多次調用,包含以下兩個部分,
(省略了組件首次加載的流程)
(1)用戶點擊 div 觸發(fā)事件,事件中調用了兩次 setState,第 1-65 行

我們看到 React 只執(zhí)行了第一個狀態(tài)更新函數(shù)(第一次
setState 的 action 參數(shù)),
s => {
debugger;
return s + 1;
}
第二次 setState 的 action 并未在這個階段執(zhí)行,而是將更新過程,放到了一個名為 update 的循環(huán)隊列中。
參考 dispatchAction L16620
function dispatchAction(fiber, queue, action) {
...
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
var pending = queue.pending;
if (pending === null) {
update.next = update; // 第一次調用 setState 時,循環(huán)隊列只有一個元素(自己指向自己)
} else {
update.next = pending.next;
pending.next = update; // <- 將 update 放到循環(huán)隊列中(邏輯見下文解釋)
}
queue.pending = update;
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
...
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
...
if (lastRenderedReducer !== null) {
...
try {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer; // <- 用來標記這個 update 元素已經(jīng)計算過了
update.eagerState = eagerState;
if (objectIs(eagerState, currentState)) {
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
...
}
}
}
...
}
...
}
update 邏輯如下,
- 每個
hook維護了一個 update quque,hook的pending屬性指向了這個 quque 的隊尾(隊尾的next為隊首) - 每次調用
setState(=dispatchAction)都會創(chuàng)建一個update節(jié)點 - 第一次調用
setState,update quque 只包含了一個元素(自己指向自己),然后設置hook.pending指向這個update元素 - 第二次調用
setState,會在 update quque 隊尾添加一個元素,再設置當前這個隊尾元素指向隊首,
hook.pending -> 當前的 update 元素
(當前的 update 元素).next -> 隊首
原隊尾.next -> 當前的 update 元素
以上這樣設置的好處是,可以從隊尾元素開始,循環(huán)獲取 next 元素,將隊列按順序處理一遍。
值得一提的是,React 采用了給 update.eagerReducer 賦值為 lastRenderedReducer 的方式,來標記這個 update 元素已經(jīng)處理過了,
update.eagerReducer = lastRenderedReducer;
這里要留意一下,下文會用到。
(2)事件完成之前,React 通過 flushSyncCallbackQueue,更新 Fiber Tree,并寫入到 DOM 中,第 67-166 行

其中
syncQueue 中保存了 performSyncWorkOnRoot,React 用它在事件結束之前更新頁面(見 前一篇 的分析)update quque 是本文介紹的內容,React 在每次調用
setState 的時候,會創(chuàng)建一個循環(huán)隊列,然后在 performSyncWorkOnRoot 的 render 階段 再執(zhí)行計算。
代碼邏輯在這里 updateReducer L15761
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
...
if (baseQueue !== null) {
...
do { // <- 從隊首開始處理 update quque
...
if (!isSubsetOfLanes(renderLanes, updateLane)) {
...
} else {
...
if (update.eagerReducer === reducer) { // 用來標記第一個 setState 已經(jīng)計算過了
newState = update.eagerState;
} else {
var action = update.action;
newState = reducer(newState, action); // 后續(xù)未計算過的 setState,會按順序執(zhí)行計算
}
}
update = update.next;
} while (update !== null && update !== first);
...
}
...
}
這里出現(xiàn)了對 update 元素 update.eagerReducer 的判定,來區(qū)分這個元素所表示的 setState 是否已經(jīng)計算過了。
所以,除了第一個 setState 是 “同步”(setState 返回之前)執(zhí)行的之外,
后續(xù)各個 setState 都是 “異步”(setState 返回后,由 React 通過 flushSyncCallbackQueue 在 render 階段) 執(zhí)行的。
4. fiber, hook, update
Fiber Tree,F(xiàn)iber Node,hook,Update Queue 四者的關系如下,

Fiber Tree 有兩棵
一棵是已寫入到的 DOM 的(稱為current),一棵是用于 render 階段處理的(稱為workInProgress)
Fiber Tree 的根節(jié)點的tag為HostRoot
兩棵 Fiber Tree 的根節(jié)點通過stateNode指向FiberRootNode,它通過containerInfo保存了 html 元素div#root
Fiber Tree 的節(jié)點有三個屬性,return指向父節(jié)點,child指向子節(jié)點,alternate指向同級的另一棵 Fiber Tree一個 React 組件可以使用多個 hook(創(chuàng)建多個獨立的狀態(tài))
hook 保存在了 Fiber Node (代表<App />元素的那個)的memorizedState屬性中,多個 hook 以鏈表形式存儲
(同層級的 Fiber Node 共用一個 hook 對象)(可能會出現(xiàn)復制的情況)
每一個 hook(比如useState)返回一個新的dispatch方法,
特定dispatch方法的每次調用,都會創(chuàng)建一個update元素,并添加到 update quque 中。
hook.queue.pending指向了 update queue 的隊尾,隊尾指向隊首(循環(huán)隊列)。組件通過
setState進行狀態(tài)更新時
只有第一個 更新 會在setState返回值之前執(zhí)行,不論是setState(action)中的action是數(shù)值還是函數(shù)
后續(xù)所有(同一個或其他 hook)的setState調用,都會將更新放到 update quque 中,
然后由 React 通過flushSyncCallbackQueue調用performSyncWorkOnRoot在 render 階段按順序執(zhí)行計算。
參考
React 初窺門徑(六):React 組件的更新過程
github: thzt/react-tour
7.1 hook 原理:多個 hook
7.2 hook 原理:多次調用


