React18之update流程從零實(shí)現(xiàn)詳解
引言
本系列是講述從0開始實(shí)現(xiàn)一個(gè)react18的基本版本。由于React
源碼通過(guò)Mono-repo 管理倉(cāng)庫(kù),我們也是用pnpm
提供的workspaces
來(lái)管理我們的代碼倉(cāng)庫(kù),打包我們使用rollup
進(jìn)行打包。
本章我們主要講解通過(guò)useState
狀態(tài)改變,引起的單節(jié)點(diǎn)update
更新階段的流程。
對(duì)比Mount階段
對(duì)比我們之前講解的mount
階段,update
階段也會(huì)經(jīng)歷大致的流程, 只是處理邏輯會(huì)有不同:
之前的章節(jié)我們主要講了reconciler
(調(diào)和) 階段中mount
階段:
beginWork
:向下調(diào)和創(chuàng)建fiberNode
樹,completeWork
:構(gòu)建離屏DOM樹以及打subtreeFlags
標(biāo)記。commitWork
:根據(jù)placement
創(chuàng)建domuseState
: 對(duì)應(yīng)調(diào)用mountState
這一節(jié)的update
階段如下:
begionWork
階段:
- 處理
ChildDeletion
的刪除的情況 - 處理節(jié)點(diǎn)移動(dòng)的情況 (abc -> bca)
completeWork
階段:
- 基于
HostText
的內(nèi)容更新標(biāo)記更新flags
- 基于
HostComponent
屬性變化標(biāo)記更新flags
commitWork
階段:
- 基于
ChildDeletion
, 遍歷被刪除的子樹 - 基于
Update
, 更新文本內(nèi)容
useState
階段:
- 實(shí)現(xiàn)相對(duì)于
mountState
的updateState
下面我們分別一一地實(shí)現(xiàn)單節(jié)點(diǎn)的update
更新流程
beginWork流程
對(duì)于單一節(jié)點(diǎn)的向下調(diào)和流程,主要在childFibers
文件中,分2種,一種是文本節(jié)點(diǎn)的處理reconcileSingleTextNode
, 一種是標(biāo)簽節(jié)點(diǎn)的處理reconcileSingleElement
。
復(fù)用fiberNode
在update
階段的話,主要有一點(diǎn)是要思考如何復(fù)用之前mount
階段已經(jīng)創(chuàng)建的fiberNode
。
我們先以reconcileSingleElement
為例子講解。
當(dāng)新的ReactElement
的type 和 key都和之前的對(duì)應(yīng)的fiberNode
都一樣的時(shí)候,才能夠進(jìn)行復(fù)用。我們先看看reconcileSingleElement
是復(fù)用的邏輯。
function reconcileSingleElement( returnFiber: FiberNode, currentFiber: FiberNode | null, element: ReactElementType ) { const key = element.key; // update的情況 <單節(jié)點(diǎn)的處理 div -> p> if (currentFiber !== null) { // key相同 if (currentFiber.key === key) { // 是react元素 if (element.$$typeof === REACT_ELEMENT_TYPE) { // type相同 if (currentFiber.type === element.type) { const existing = useFiber(currentFiber, element.props); existing.return = returnFiber; return existing; } } } } }
- 首先我們需要判斷
currentFiber
是否存在,當(dāng)存在的時(shí)候,說(shuō)明是進(jìn)入了update
階段。 - 根據(jù)
currentFiber
和element
的tag 和 type判斷,如果相同才可以復(fù)用。 - 通過(guò)雙緩存樹(
useFiber
)去復(fù)用fiberNode。
useFiber
復(fù)用的邏輯本質(zhì)就是調(diào)用了useFiber
, 本質(zhì)上,它是通過(guò)雙緩存書指針alternate
,它接受已經(jīng)渲染對(duì)應(yīng)的fiberNode
以及新的Props
巧妙的運(yùn)用我們之前創(chuàng)建wip
的邏輯,可以很好的復(fù)用fiberNode
。
/** * 雙緩存樹原理:基于當(dāng)前的fiberNode創(chuàng)建一個(gè)新的fiberNode, 而不用去調(diào)用new FiberNode * @param {FiberNode} fiber 正在展示的fiberNode * @param {Props} pendingProps 新的Props * @returns {FiberNode} */ function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode { const clone = createWorkInProgress(fiber, pendingProps); clone.index = 0; clone.sibling = null; return clone; }
對(duì)于reconcileSingleTextNode
刪除舊的和新建fiberNode
當(dāng)不能夠復(fù)用fiberNode
的時(shí)候,我們除了要像mount
的時(shí)候新建fiberNode
(已經(jīng)有的邏輯),還需要?jiǎng)h除舊的fiberNode
。
我們先以reconcileSingleElement
為例子講解。
在beginWork
階段,我們只需要標(biāo)記刪除flags
。以下2種情況我們需要額外的標(biāo)記舊fiberNode
刪除
key
不同key
相同,type
不同
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) { if (!shouldTrackEffects) { return; } const deletions = returnFiber.deletions; if (deletions === null) { // 當(dāng)前父fiber還沒(méi)有需要?jiǎng)h除的子fiber returnFiber.deletions = [childToDelete]; returnFiber.flags |= ChildDeletion; } else { deletions.push(childToDelete); } }
我們將需要?jiǎng)h除的節(jié)點(diǎn),通過(guò)數(shù)組形式賦值到父節(jié)點(diǎn)deletions
中,并標(biāo)記ChildDeletion
有節(jié)點(diǎn)需要?jiǎng)h除。
對(duì)于reconcileSingleTextNode
, 當(dāng)渲染視圖中是HostText
就可以直接復(fù)用。整體代碼如下:
function reconcileSingleTextNode( returnFiber: FiberNode, currentFiber: FiberNode | null, content: string | number ): FiberNode { // update if (currentFiber !== null) { // 類型沒(méi)有變,可以復(fù)用 if (currentFiber.tag === HostText) { const existing = useFiber(currentFiber, { content }); existing.return = returnFiber; return existing; } // 刪掉之前的 (之前的div, 現(xiàn)在是hostText) deleteChild(returnFiber, currentFiber); } const fiber = new FiberNode(HostText, { content }, null); fiber.return = returnFiber; return fiber; }
completeWork流程
當(dāng)在beginWork
做好相應(yīng)的刪除和移動(dòng)標(biāo)記后,在completeWork
主要是做更新的標(biāo)記。
對(duì)于單一的節(jié)點(diǎn)來(lái)說(shuō),更新標(biāo)記分為2種,
- 第一種是文本元素的更新,主要是新舊文本內(nèi)容的不一樣。
- 第二種是類似div的屬性等更新。這個(gè)我們下一節(jié)進(jìn)行講解。
這里我們只對(duì)HostText
中的類型進(jìn)行講解。
case HostText: if (current !== null && wip.stateNode) { //update const oldText = current.memoizedProps.content; const newText = newProps.content; if (oldText !== newText) { // 標(biāo)記更新 markUpdate(wip); } } else { // 1. 構(gòu)建DOM const instance = createTextInstance(newProps.content); // 2. 將DOM插入到DOM樹中 wip.stateNode = instance; } bubbleProperties(wip); return null;
從上面我們可以看出,我們根據(jù)文本內(nèi)容的不同,進(jìn)行當(dāng)前節(jié)點(diǎn)wip
進(jìn)行標(biāo)記。
function markUpdate(fiber: FiberNode) { fiber.flags |= Update; }
commitWork流程
通過(guò)beginWork
和completeWork
之后,我們得到了相應(yīng)的標(biāo)記。在commitWork
階段,我們就需要根據(jù)相應(yīng)標(biāo)記去處理不同的邏輯。本節(jié)主要講解更新
和刪除
階段的處理。
更新update
在之前的章節(jié)中,我們講解了commitWork
的mount
階段,我們現(xiàn)在根據(jù)update
的flag進(jìn)行邏輯處理。
// flags update if ((flags & Update) !== NoFlags) { commitUpdate(finishedWork); finishedWork.flags &= ~Update; }
commitUpdate
對(duì)于文本節(jié)點(diǎn),commitUpdate
主要是根據(jù)新的文本內(nèi)容,更新之前的dom的文本內(nèi)容。
export function commitUpdate(fiber: FiberNode) { switch (fiber.tag) { case HostText: const text = fiber.memoizedProps.content; return commitTextUpdate(fiber.stateNode, text); } } export function commitTextUpdate(textInstance: TestInstance, content: string) { textInstance.textContent = content; }
刪除ChildDeletion
在beginWork
過(guò)程中,對(duì)于存在要?jiǎng)h除的子節(jié)點(diǎn),我們會(huì)保存在當(dāng)前父節(jié)點(diǎn)的deletions
, 所以在刪除階段,我們需要根據(jù)當(dāng)前節(jié)點(diǎn)的deletions
屬性進(jìn)行對(duì)要?jiǎng)h除的節(jié)點(diǎn)進(jìn)行不同的處理。
// flags childDeletion if ((flags & ChildDeletion) !== NoFlags) { const deletions = finishedWork.deletions; if (deletions !== null) { deletions.forEach((childToDelete) => { commitDeletion(childToDelete); }); } finishedWork.flags &= ~ChildDeletion; }
如果當(dāng)前節(jié)點(diǎn)存在要?jiǎng)h除的子節(jié)點(diǎn)的話,我們需要對(duì)每一個(gè)子節(jié)點(diǎn)進(jìn)行commitDeletion
的操作。
commitDeletion
commitDeletion
函數(shù)的是對(duì)每一個(gè)要?jiǎng)h除的子節(jié)點(diǎn)進(jìn)行處理。它的主要功能有幾點(diǎn):
- 對(duì)于不同類型的
fiberNode
, 當(dāng)節(jié)點(diǎn)刪除的時(shí)候,自身和所有子節(jié)點(diǎn)都需要執(zhí)行的不同的卸載邏輯。例如:函數(shù)組件的useEffect
的return函數(shù)執(zhí)行,ref
的解綁,class組件的componentUnmount
等邏輯處理。 - 由于
fiberNode
和dom節(jié)點(diǎn)不是一一對(duì)應(yīng)的,所以要找到fiberNode
對(duì)應(yīng)的dom節(jié)點(diǎn),然后再執(zhí)行刪除dom節(jié)點(diǎn)的操作。 - 最后將刪除的節(jié)點(diǎn)的
child
和return
指向刪掉。
基于上面的2點(diǎn)分析,我們很容易就想到,commitDeletion
肯定會(huì)執(zhí)行DFS向下遍歷,進(jìn)行不同子節(jié)點(diǎn)的刪除邏輯處理。
/** * rootHostNode 找到對(duì)應(yīng)的DOM節(jié)點(diǎn)。 * commitNestedComponent DFS遍歷節(jié)點(diǎn)的進(jìn)行卸載相關(guān)的邏輯 * @param {FiberNode} childToDelete */ function commitDeletion(childToDelete: FiberNode) { let rootHostNode: FiberNode | null = null; // 遞歸子樹 commitNestedComponent(childToDelete, (unmountFiber) => { switch (unmountFiber.tag) { case HostComponent: if (rootHostNode === null) { rootHostNode = unmountFiber; } // TODO: 解綁ref return; case HostText: if (rootHostNode === null) { rootHostNode = unmountFiber; } return; case FunctionComponent: // TODO: useEffect unmount 解綁ref return; default: if (__DEV__) { console.warn("未處理的unmount類型", unmountFiber); } break; } }); // 移除rootHostNode的DOM if (rootHostNode !== null) { const hostParent = getHostParent(childToDelete); if (hostParent !== null) { removeChild((rootHostNode as FiberNode).stateNode, hostParent); } } childToDelete.return = null; childToDelete.child = null; }
commitNestedComponent
commitNestedComponent
中主要是完成我們上面說(shuō)的2點(diǎn)。
- DFS深度遍歷子節(jié)點(diǎn)
- 找到當(dāng)前要?jiǎng)h除的
fiberNode
對(duì)應(yīng)的真正的DOM
節(jié)點(diǎn)
接受2個(gè)參數(shù)。1. 當(dāng)前的fiberNode
, 2. 遞歸到不同的子節(jié)點(diǎn)的同時(shí),需要執(zhí)行的回調(diào)函數(shù)執(zhí)行不同的卸載流程。
function commitNestedComponent( root: FiberNode, onCommitUnmount: (fiber: FiberNode) => void ) { let node = root; while (true) { onCommitUnmount(node); if (node.child !== null) { // 向下遍歷 node.child.return = node; node = node.child; continue; } if (node === root) { // 終止條件 return; } while (node.sibling === null) { if (node.return === null || node.return === root) { return; } // 向上歸 node = node.return; } node.sibling.return = node.return; node = node.sibling; } }
這里可能比較繞,我們下面通過(guò)幾個(gè)例子總結(jié)一下,這個(gè)過(guò)程的主要流程。
總結(jié)
如果按照如下的結(jié)構(gòu),要?jiǎng)h除外層div
元素,會(huì)經(jīng)歷如下的流程
<div> <Child /> <span>hcc</span> yx </div> function Child() { return <div>hello world</div> }
div
的fiberNode的父節(jié)的標(biāo)記ChildDeletion
以及存放到deletions
中。- 當(dāng)執(zhí)行到
commitWork
階段的時(shí)候,遍歷deletions
數(shù)組。 - 執(zhí)行的div對(duì)應(yīng)的
HostComponent
, 然后執(zhí)行commitDeletion
- 在
commitDeletion
中執(zhí)行commitNestedComponent
向下DFS遍歷。 - 在遍歷的過(guò)程中,每一個(gè)節(jié)點(diǎn)都是執(zhí)行一個(gè)回調(diào)函數(shù),基于不同的類型執(zhí)行不同的刪除操作,以及記錄我們要?jiǎng)h除的Dom節(jié)點(diǎn)對(duì)應(yīng)的fiberNode。
- 所以首先是
div
執(zhí)行onCommitUnmount, 由于它是HostComponent
,所以將rootHostNode
賦值給了div
- 向下遞歸到
Child
節(jié)點(diǎn),由于它存在子節(jié)點(diǎn),繼續(xù)遞歸到child-div
節(jié)點(diǎn),繼續(xù)遍歷到hello world
節(jié)點(diǎn)。它不存在子節(jié)點(diǎn)。 - 然后找到
Child
的兄弟節(jié)點(diǎn),以此執(zhí)行,先子后兄。直到回到div
節(jié)點(diǎn)。
下一節(jié)預(yù)告
下一節(jié)我們講解通過(guò)useState
改變狀態(tài)后,如何更新節(jié)點(diǎn)以及函數(shù)組件hooks是如何保存數(shù)據(jù)的。
以上就是React18之update流程從零實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于React18 update流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解React Native 屏幕適配(炒雞簡(jiǎn)單的方法)
React Native 可以開發(fā) ios 和 android 的 app,在開發(fā)過(guò)程中,勢(shì)必會(huì)遇上屏幕適配,這篇文章主要介紹了詳解React Native 屏幕適配(炒雞簡(jiǎn)單的方法),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06react之umi配置國(guó)際化語(yǔ)言locale的踩坑記錄
這篇文章主要介紹了react之umi配置國(guó)際化語(yǔ)言locale的踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02淺談React + Webpack 構(gòu)建打包優(yōu)化
本篇文章主要介紹了淺談React + Webpack 構(gòu)建打包優(yōu)化,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01React?Native系列之Recyclerlistview使用詳解
這篇文章主要為大家介紹了React?Native系列之Recyclerlistview使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10react項(xiàng)目中redux的調(diào)試工具不起作用的解決
這篇文章主要介紹了react項(xiàng)目中redux的調(diào)試工具不起作用的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01react實(shí)現(xiàn)導(dǎo)航欄二級(jí)聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)導(dǎo)航欄二級(jí)聯(lián)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03React Grid Layout基礎(chǔ)使用示例教程
React Grid Layout是一個(gè)用于在React應(yīng)用程序中創(chuàng)建可拖拽和可調(diào)整大小的網(wǎng)格布局的庫(kù),通過(guò)使用React Grid Layout,我們可以輕松地創(chuàng)建自適應(yīng)的網(wǎng)格布局,并實(shí)現(xiàn)拖拽和調(diào)整大小的功能,本文介紹了React Grid Layout的基礎(chǔ)使用方法,感興趣的朋友一起看看吧2024-02-02原生實(shí)現(xiàn)一個(gè)react-redux的代碼示例
這篇文章主要介紹了原生實(shí)現(xiàn)一個(gè)react-redux的代碼示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06