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

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

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

useState觸發(fā)更新(dispatch)
當(dāng)我們執(zhí)行setNum(3)的時(shí)候,我們之前講過(guò)相當(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. 開(kāi)始調(diào)度
}
當(dāng)再次執(zhí)行到函數(shù)組件App的時(shí)候,會(huì)執(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ù),重新會(huì)調(diào)用useState
const [num, setNum] = useState(100);
updateState
在HooksDispatcherOnUpdate中,useState對(duì)應(yīng)的是updateState。對(duì)比于mountState的話,updateState主要是:
- hook的數(shù)據(jù)從哪里來(lái)
- 會(huì)有2種情況執(zhí)行,交互階段觸發(fā),render的時(shí)候觸發(fā)
本節(jié)主要是分析交互階段的觸發(fā)的邏輯。
hook數(shù)據(jù)從哪里來(lái)
對(duì)比mountState中,我們可以通過(guò)新建hook數(shù)據(jù)結(jié)構(gòu)。這個(gè)時(shí)候雙緩存樹(shù)的結(jié)構(gòu)就可以解決,還記得我們之前的章節(jié)講的react將正在渲染的和正在進(jìn)行的分2個(gè)樹(shù),通過(guò)alternate進(jìn)行鏈接。整體結(jié)構(gòu)如下圖:

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

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

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

