React18從0實(shí)現(xiàn)dispatch?update流程
引言
本系列是講述從0開始實(shí)現(xiàn)一個(gè)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)用。
我們有如下代碼,在初始化的時(shí)候執(zhí)行useState和調(diào)用setNum
的時(shí)候,是如何更新的。
function App() { const [num, setNum] = useState(100); window.setNum = setNum; return <div>{num}</div>; }
hooks原理
基于useState
我們來講講hook在初始化和更新階段的區(qū)別。以及react是如何做到hook不能在條件語句和函數(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ù)來源
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
。 我們首先來看看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é)果來推算這個(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就需要通過next
屬性,將hook串聯(lián)起來。
在這個(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, 說明是第一個(gè)hook執(zhí)行。所以我們將賦值workInProgressHook
正在執(zhí)行的hook, 同時(shí)將FC fiberNode
的memoizedState
指向第一個(gè)hook。此時(shí)就生成了如下圖的結(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對(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)單,就是通過傳入的參數(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
保存起來,方便之后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
它接受三個(gè)參數(shù),因?yàn)槲覀冃枰朗菑哪囊粋€(gè)fiberNode開始調(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. 開始調(diào)度 }
所以我們每次執(zhí)行setState
的時(shí)候,等同于執(zhí)行上面函數(shù),但是我們只需要傳遞action
就可以,前2個(gè)參數(shù),已經(jīng)通過bind
綁定。
執(zhí)行dispatch
后,開始新一輪的調(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í)候,我們之前講過相當(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
的時(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ù)從哪里來
- 會(huì)有2種情況執(zhí)行,交互階段觸發(fā),render的時(shí)候觸發(fā)
本節(jié)主要是分析交互階段的觸發(fā)的邏輯。
hook數(shù)據(jù)從哪里來
對(duì)比mountState
中,我們可以通過新建hook
數(shù)據(jù)結(jié)構(gòu)。這個(gè)時(shí)候雙緩存樹的結(jié)構(gòu)就可以解決,還記得我們之前的章節(jié)講的react將正在渲染的和正在進(jìn)行的分2個(gè)樹,通過alternate
進(jìn)行鏈接。整體結(jié)構(gòu)如下圖:
還記得我們mount
的時(shí)候說過,fiberNode.memoizedState
的指向保存著hook的數(shù)據(jù)。
所以我們可以通過currentlyRenderingFiber?.alternate
中的memoizedState
去查找對(duì)應(yīng)的hook數(shù)據(jù)。
updateWorkInProgressHook
更新階段hook
的數(shù)據(jù)獲取是通過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é)如下:
- 剛開始
currentHook
為null, 通過alternate
指向memoizedState
獲取到正在渲染中的hook數(shù)據(jù),賦值給nextCurrentHook
- 將
currentHook
賦值為nextCurrentHook
, 記錄更新的數(shù)據(jù)來源,方便之后的hook,通過next
連接起來。 - 賦值
workInProgressHook
標(biāo)記正在執(zhí)行的hook
這里有一個(gè)難點(diǎn),就是nextCurrentHook === null
的時(shí)候,我們可以拋出錯(cuò)誤。
hook在條件語句中報(bào)錯(cuò)
我們曉得hook是不能在條件語句中執(zhí)行的。那是如何做到報(bào)錯(cuò)的呢?接下來我們根據(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
,我們來分析一下。
nextCurrentHook
=currentHook
=m-hook1
,第一次后currentHook
不為nullnextCurrentHook
等于m-hook2
nextCurrentHook
等于m-hook3
- 第四次的時(shí)候
nextCurrentHook
=m-hook3.next
= null, 所以就會(huì)走到報(bào)錯(cuò)的邏輯。
useState計(jì)算
上一部分我們已經(jīng)知道了update的時(shí)候,hook的數(shù)據(jù)來源,我們現(xiàn)在得到數(shù)據(jù)了,那如何通過之前的數(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í)候,我們的雙緩存樹還沒有建立起來,在第一次更新之后,雙緩存樹就建立完成。
之后每一次調(diào)和生成子fiberNode
的時(shí)候,都會(huì)利用alternate
指針去重復(fù)利用相同type和相同key的節(jié)點(diǎn)。
例如初始化的時(shí)候num
的值為3, 通過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í)候,第二次更新后,雙緩存樹已經(jīng)建立。會(huì)利用之前右邊的4
的fiberNode tree
,進(jìn)行下一輪渲染。
總結(jié)
此節(jié)我們主要是講了hook是如何存放數(shù)據(jù)的,以及mount
階段和update
階段不同的存放,也講解了通過dispatch
調(diào)用后,react是如何更新的。以及雙緩存樹在第一次更新后是如何建立的。
以上就是React18從0實(shí)現(xiàn)dispatch update流程的詳細(xì)內(nèi)容,更多關(guān)于React18 dispatch update流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react實(shí)現(xiàn)列表滾動(dòng)組件功能
在開發(fā)項(xiàng)目的時(shí)候,從服務(wù)端獲取到數(shù)據(jù)列表后,展示給用戶看,需要實(shí)現(xiàn)數(shù)據(jù)自動(dòng)滾動(dòng)效果,怎么實(shí)現(xiàn)呢,下面小編給大家分享react實(shí)現(xiàn)列表滾動(dòng)組件功能實(shí)現(xiàn)代碼,感興趣的朋友一起看看吧2023-09-09Remix集成antd和pro-components的過程示例
這篇文章主要為大家介紹了Remix集成antd和pro-components的過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03React?Hook?Form?優(yōu)雅處理表單使用指南
這篇文章主要為大家介紹了React?Hook?Form?優(yōu)雅處理表單使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03React踩坑之a(chǎn)ntd輸入框rules中的required=true問題
這篇文章主要介紹了React踩坑之a(chǎn)ntd輸入框rules中的required=true問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06淺談React中的元素、組件、實(shí)例和節(jié)點(diǎn)
這篇文章主要介紹了淺談React中的元素、組件、實(shí)例和節(jié)點(diǎn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02