React18從0實現(xiàn)dispatch?update流程
引言
本系列是講述從0開始實現(xiàn)一個react18的基本版本。由于React源碼通過Mono-repo 管理倉庫,我們也是用pnpm提供的workspaces來管理我們的代碼倉庫,打包我們使用rollup進(jìn)行打包。
上一節(jié)中我們講解了update的過程中,begionWork和completeWork、commitWork的具體執(zhí)行流程。本節(jié)主要是講解
hooks是如何存放數(shù)據(jù)的,以及一些hooks的規(guī)則。- 一次
dispatch觸發(fā)的更新整體流程,雙緩存樹的運(yùn)用。
我們有如下代碼,在初始化的時候執(zhí)行useState和調(diào)用setNum的時候,是如何更新的。
function App() {
const [num, setNum] = useState(100);
window.setNum = setNum;
return <div>{num}</div>;
}
hooks原理
基于useState我們來講講hook在初始化和更新階段的區(qū)別。以及react是如何做到hook不能在條件語句和函數(shù)組件外部使用的。
在react中,對于同一個hook,在不同的環(huán)境都是有不同的集合區(qū)分,這樣就可以做到基于不同的執(zhí)行環(huán)境的不同判斷。
首先有幾個名詞:
currentlyRenderingFiber: 記錄當(dāng)前正在執(zhí)行的函數(shù)組件的fiberNode
workInProgressHook: 當(dāng)前正在執(zhí)行的hook
currentHook:更新的時候的數(shù)據(jù)來源
memoizedState: 對于fiberNode.memoizedState是存放hooks的指向。對于hook.memoizedState就是存放數(shù)據(jù)的地方。
hook的結(jié)構(gòu)如下圖:

useState初始化(mount)
我們知道當(dāng)beginWork階段的時候,對于函數(shù)組件,會執(zhí)行renderWithHooks去生成當(dāng)前對應(yīng)的子fiberNode。 我們首先來看看renderWithHooks的邏輯部分。
export function renderWithHooks(wip: FiberNode) {
// 賦值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}
const Component = wip.type;
const props = wip.pendingProps;
const children = Component(props);
// 重置操作
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
return children;
}
首先會將currentlyRenderingFiber賦值給當(dāng)前的FC的fiberNode,然后重置掉memoizedState, 因為初始化的時候會生成,更新的時候會根據(jù)初始化的時候生成。
可以看到對于mount階段,主要是執(zhí)行HooksDispatcherOnMount, 他實際上是一個hook集合。我們主要看看mountState的邏輯處理。
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
};
mountState
對于第一次執(zhí)行useState, 我們根據(jù)結(jié)果來推算這個函數(shù)的主要功能。useState需要返回2個值,第一個是state,第二個是可以引發(fā)更新的setState。所以mountState的主要功能:
- 根據(jù)傳入的
initialState生成新的state - 返回dispatch,便于之后調(diào)用更新state
基于hook的結(jié)構(gòu)圖,我們知道每一個hook有三個屬性, 所以我們首先要有一個函數(shù)去生成對應(yīng)的hook的結(jié)構(gòu)。
interface Hook {
memoizedState: any;
updateQueue: unknown;
next: Hook | null;
}
mountWorkInProgressHook
mountWorkInProgressHook這個函數(shù)主要是構(gòu)建hook的數(shù)據(jù)。分為2種情況,第一種是第一個hook, 第二種是不是第一個hook就需要通過next屬性,將hook串聯(lián)起來。
在這個函數(shù)中,我們就可以判斷當(dāng)前執(zhí)行的hook,是否是在函數(shù)中執(zhí)行的。如果是在函數(shù)中執(zhí)行的話,在執(zhí)行函數(shù)組件的時候,我們將currentlyRenderingFiber 賦值給了wip, 如果是直接調(diào)用的話,currentlyRenderingFiber則為null,我們就可以拋出錯誤。
/**
* mount獲取當(dāng)前hook對應(yīng)的數(shù)據(jù)
*/
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
updateQueue: null,
next: null,
};
if (workInProgressHook === null) {
// mount時,第一個hook
if (currentlyRenderingFiber === null) {
throw new Error("請在函數(shù)組件內(nèi)調(diào)用hook");
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// mount時,后續(xù)的hook
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}
當(dāng)?shù)谝淮螆?zhí)行的時候,workInProgressHook的值為null, 說明是第一個hook執(zhí)行。所以我們將賦值workInProgressHook正在執(zhí)行的hook, 同時將FC fiberNode的memoizedState指向第一個hook。此時就生成了如下圖的結(jié)構(gòu):

處理hook數(shù)據(jù)
通過mountWorkInProgressHook我們得到當(dāng)前的hook結(jié)構(gòu)后,需要處理memoizedState以及updateQueue的值。
function mountState<State>(
initialState: (() => State) | State
): [State, Dispatch<State>] {
// 找到當(dāng)前useState對應(yīng)的hook數(shù)據(jù)
const hook = mountWorkInProgressHook();
let memoizedState;
if (initialState instanceof Function) {
memoizedState = initialState();
} else {
memoizedState = initialState;
}
// useState是可以觸發(fā)更新的
const queue = createUpdateQueue<State>();
hook.updateQueue = queue;
hook.memoizedState = memoizedState;
//@ts-ignore
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;
return [memoizedState, dispatch];
}
從上面的代碼中,我們可以看出memoizedState的處理很簡單,就是通過傳入的參數(shù),進(jìn)行賦值處理,重點在于如何生成dispatch
生成dispatch
因為觸發(fā)dispatch的時候,react是要觸發(fā)更新的,所以必然會和調(diào)度有關(guān)。
由于要觸發(fā)更新,我們就需要創(chuàng)建觸發(fā)更新的隊列
- 執(zhí)行
createUpdateQueue()生成更新隊列。 - 將更新隊列賦值給當(dāng)前
hook保存起來,方便之后update使用。 - 將生成的
dispatch保存起來,方便之后update使用。
// useState是可以觸發(fā)更新的 const queue = createUpdateQueue<State>(); hook.updateQueue = queue; const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue); queue.dispatch = dispatch;
主要是看如何生成dispatch的邏輯,通過調(diào)用dispatchSetState它接受三個參數(shù),因為我們需要知道是從哪一個fiberNode開始調(diào)度的,所以當(dāng)前的fiberNode是肯定看需要的。更新隊列queue也是需要的,用于執(zhí)行dispatch的時候觸發(fā)更新。
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
const update = createUpdate(action); // 1. 創(chuàng)建update
enqueueUpdate(updateQueue, update); // 2. 將更新放入隊列中
scheduleUpdateOnFiber(fiber); // 3. 開始調(diào)度
}
所以我們每次執(zhí)行setState的時候,等同于執(zhí)行上面函數(shù),但是我們只需要傳遞action就可以,前2個參數(shù),已經(jīng)通過bind綁定。
執(zhí)行dispatch后,開始新一輪的調(diào)度,調(diào)和。
更新的總結(jié)
從上面的代碼,我們可以看出我們首先是執(zhí)行了createUpdateQueue, 然后執(zhí)行了createUpdate, 然后enqueueUpdate。這里總結(jié)一下這些函數(shù)調(diào)用。
createUpdateQueue本質(zhì)上就創(chuàng)建了一個對象,用于保存值
return {
shared: {
pending: null,
},
dispatch: null,
}
createUpdate就是也是返回一個對象。
return {
action,
};
enqueueUpdate就是將createUpdateQueue的pending 賦值。
{
updateQueue.shared.pending = update;
};
最后我們生成的單個hook結(jié)構(gòu)如下圖:

useState觸發(fā)更新(dispatch)
當(dāng)我們執(zhí)行setNum(3)的時候,我們之前講過相當(dāng)于是執(zhí)行了下面函數(shù), 將傳遞3為action的值。
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
const update = createUpdate(action);
enqueueUpdate(updateQueue, update);
scheduleUpdateOnFiber(fiber); // 3. 開始調(diào)度
}
當(dāng)再次執(zhí)行到函數(shù)組件App的時候,會執(zhí)行renderWithHooks如下的邏輯。將 currentDispatcher.current賦值給HooksDispatcherOnUpdate。
// 賦值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}
然后執(zhí)行App函數(shù),重新會調(diào)用useState
const [num, setNum] = useState(100);
updateState
在HooksDispatcherOnUpdate中,useState對應(yīng)的是updateState。對比于mountState的話,updateState主要是:
- hook的數(shù)據(jù)從哪里來
- 會有2種情況執(zhí)行,交互階段觸發(fā),render的時候觸發(fā)
本節(jié)主要是分析交互階段的觸發(fā)的邏輯。
hook數(shù)據(jù)從哪里來
對比mountState中,我們可以通過新建hook數(shù)據(jù)結(jié)構(gòu)。這個時候雙緩存樹的結(jié)構(gòu)就可以解決,還記得我們之前的章節(jié)講的react將正在渲染的和正在進(jìn)行的分2個樹,通過alternate進(jìn)行鏈接。整體結(jié)構(gòu)如下圖:

還記得我們mount的時候說過,fiberNode.memoizedState的指向保存著hook的數(shù)據(jù)。
所以我們可以通過currentlyRenderingFiber?.alternate中的memoizedState去查找對應(yīng)的hook數(shù)據(jù)。
updateWorkInProgressHook
更新階段hook的數(shù)據(jù)獲取是通過updateWorkInProgressHook執(zhí)行的。
function updateWorkInProgressHook(): Hook {
// TODO render階段觸發(fā)的更新
let nextCurrentHook: Hook | null;
// FC update時的第一個hook
if (currentHook === null) {
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
nextCurrentHook = current?.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// FC update時候,后續(xù)的hook
nextCurrentHook = currentHook.next;
}
if (nextCurrentHook === null) {
// mount / update u1 u2 u3
// update u1 u2 u3 u4
throw new Error(
`組件${currentlyRenderingFiber?.type}本次執(zhí)行時的Hook比上次執(zhí)行的多`
);
}
currentHook = nextCurrentHook as Hook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
updateQueue: currentHook.updateQueue,
next: null,
};
if (workInProgressHook === null) {
// update時,第一個hook
if (currentlyRenderingFiber === null) {
throw new Error("請在函數(shù)組件內(nèi)調(diào)用hook");
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// update時,后續(xù)的hook
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}
主要邏輯總結(jié)如下:
- 剛開始
currentHook為null, 通過alternate指向memoizedState獲取到正在渲染中的hook數(shù)據(jù),賦值給nextCurrentHook - 將
currentHook賦值為nextCurrentHook, 記錄更新的數(shù)據(jù)來源,方便之后的hook,通過next連接起來。 - 賦值
workInProgressHook標(biāo)記正在執(zhí)行的hook
這里有一個難點,就是nextCurrentHook === null的時候,我們可以拋出錯誤。
hook在條件語句中報錯
我們曉得hook是不能在條件語句中執(zhí)行的。那是如何做到報錯的呢?接下來我們根據(jù)上面的updateWorkProgressHook源碼分析。假如,偽代碼如下所示: 在mount階段的時候,是3個hook,在執(zhí)行setNum(100),update階段4個。
const [num, setNum] = useState(99);
const [num2, setNum] = useState(101);
const [num3, setNum] = useState(102);
if(num === 100) {
const [num4, setNum] = useState(103);
}
這里我們就會執(zhí)行四次updateWorkProgressHook,我們來分析一下。
nextCurrentHook=currentHook=m-hook1,第一次后currentHook不為nullnextCurrentHook等于m-hook2nextCurrentHook等于m-hook3- 第四次的時候
nextCurrentHook=m-hook3.next= null, 所以就會走到報錯的邏輯。

useState計算
上一部分我們已經(jīng)知道了update的時候,hook的數(shù)據(jù)來源,我們現(xiàn)在得到數(shù)據(jù)了,那如何通過之前的數(shù)據(jù),計算出新的數(shù)據(jù)呢?
- 在執(zhí)行
setNum(action)后,我們知道action存放在queue.shared.pending中 - 而
queue是存放在對應(yīng)hook的updateQueue中。所以我們可以拿到action - 第三步就是去消費(fèi)
action,即執(zhí)行processUpdateQueue, 傳入上一次的state, 以及我們這次接受的action,計算最新的值。
function updateState<State>(): [State, Dispatch<State>] {
// 找到當(dāng)前useState對應(yīng)的hook數(shù)據(jù)
const hook = updateWorkInProgressHook();
// 計算新的state邏輯
const queue = hook.updateQueue as UpdateQueue<State>;
const pending = queue.shared.pending;
if (pending !== null) {
const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
hook.memoizedState = memoizedState;
}
return [hook.memoizedState, queue.dispatch as Dispatch<State>];
}
這樣,我們就在渲染的時候拿到了最新的值,以及重新返回的dispatch。
雙緩存樹
在第一次更新的時候,我們的雙緩存樹還沒有建立起來,在第一次更新之后,雙緩存樹就建立完成。
之后每一次調(diào)和生成子fiberNode的時候,都會利用alternate指針去重復(fù)利用相同type和相同key的節(jié)點。
例如初始化的時候num的值為3, 通過setNum(4)調(diào)用第一次更新后。首先會創(chuàng)建一個wip tree

在執(zhí)行完commitWork后,屏幕上渲染為4后,root.current的指向會被修改 為wip tree。
當(dāng)我們再setNum(5)的時候,第二次更新后,雙緩存樹已經(jīng)建立。會利用之前右邊的4的fiberNode tree,進(jìn)行下一輪渲染。
總結(jié)
此節(jié)我們主要是講了hook是如何存放數(shù)據(jù)的,以及mount階段和update階段不同的存放,也講解了通過dispatch調(diào)用后,react是如何更新的。以及雙緩存樹在第一次更新后是如何建立的。
以上就是React18從0實現(xiàn)dispatch update流程的詳細(xì)內(nèi)容,更多關(guān)于React18 dispatch update流程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入理解React Native原生模塊與JS模塊通信的幾種方式
本篇文章主要介紹了深入理解React Native原生模塊與JS模塊通信的幾種方式,具有一定的參考價值,有興趣的可以了解一下2017-07-07
在Ant Design Pro登錄功能中集成圖形驗證碼組件的方法步驟
這篇文章主要介紹了在Ant Design Pro登錄功能中集成圖形驗證碼組件的方法步驟,這里的登錄功能其實就是一個表單提交,實現(xiàn)起來也很簡單,具體實例代碼跟隨小編一起看看吧2021-05-05
React根據(jù)當(dāng)前頁面路由進(jìn)行自動高亮示例代碼
要根據(jù)當(dāng)前頁面路由自動高亮頂部菜單項,可以使用 React Router 的 useLocation 鉤子來獲取當(dāng)前路徑,并根據(jù)路徑動態(tài)設(shè)置菜單項的高亮效果,本文給大家介紹了一個完整的示例,展示如何根據(jù)當(dāng)前頁面路由自動高亮頂部菜單項,需要的朋友可以參考下2024-07-07

