React18之update流程從零實現(xiàn)詳解
引言
本系列是講述從0開始實現(xiàn)一個react18的基本版本。由于React源碼通過Mono-repo 管理倉庫,我們也是用pnpm提供的workspaces來管理我們的代碼倉庫,打包我們使用rollup進(jìn)行打包。
本章我們主要講解通過useState狀態(tài)改變,引起的單節(jié)點update更新階段的流程。
對比Mount階段
對比我們之前講解的mount階段,update階段也會經(jīng)歷大致的流程, 只是處理邏輯會有不同:
之前的章節(jié)我們主要講了reconciler(調(diào)和) 階段中mount階段:
beginWork:向下調(diào)和創(chuàng)建fiberNode樹,completeWork:構(gòu)建離屏DOM樹以及打subtreeFlags標(biāo)記。commitWork:根據(jù)placement創(chuàng)建domuseState: 對應(yīng)調(diào)用mountState
這一節(jié)的update階段如下:
begionWork階段:
- 處理
ChildDeletion的刪除的情況 - 處理節(jié)點移動的情況 (abc -> bca)
completeWork階段:
- 基于
HostText的內(nèi)容更新標(biāo)記更新flags - 基于
HostComponent屬性變化標(biāo)記更新flags
commitWork階段:
- 基于
ChildDeletion, 遍歷被刪除的子樹 - 基于
Update, 更新文本內(nèi)容
useState階段:
- 實現(xiàn)相對于
mountState的updateState
下面我們分別一一地實現(xiàn)單節(jié)點的update更新流程
beginWork流程
對于單一節(jié)點的向下調(diào)和流程,主要在childFibers文件中,分2種,一種是文本節(jié)點的處理reconcileSingleTextNode, 一種是標(biāo)簽節(jié)點的處理reconcileSingleElement。
復(fù)用fiberNode
在update階段的話,主要有一點是要思考如何復(fù)用之前mount階段已經(jīng)創(chuàng)建的fiberNode。
我們先以reconcileSingleElement為例子講解。
當(dāng)新的ReactElement的type 和 key都和之前的對應(yīng)的fiberNode都一樣的時候,才能夠進(jìn)行復(fù)用。我們先看看reconcileSingleElement是復(fù)用的邏輯。
function reconcileSingleElement(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
element: ReactElementType
) {
const key = element.key;
// update的情況 <單節(jié)點的處理 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)存在的時候,說明是進(jìn)入了update階段。 - 根據(jù)
currentFiber和element的tag 和 type判斷,如果相同才可以復(fù)用。 - 通過雙緩存樹(
useFiber)去復(fù)用fiberNode。
useFiber
復(fù)用的邏輯本質(zhì)就是調(diào)用了useFiber, 本質(zhì)上,它是通過雙緩存書指針alternate,它接受已經(jīng)渲染對應(yīng)的fiberNode以及新的Props 巧妙的運用我們之前創(chuàng)建wip的邏輯,可以很好的復(fù)用fiberNode。
/**
* 雙緩存樹原理:基于當(dāng)前的fiberNode創(chuàng)建一個新的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;
}
對于reconcileSingleTextNode
刪除舊的和新建fiberNode
當(dāng)不能夠復(fù)用fiberNode的時候,我們除了要像mount的時候新建fiberNode(已經(jīng)有的邏輯),還需要刪除舊的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還沒有需要刪除的子fiber
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
我們將需要刪除的節(jié)點,通過數(shù)組形式賦值到父節(jié)點deletions中,并標(biāo)記ChildDeletion有節(jié)點需要刪除。
對于reconcileSingleTextNode, 當(dāng)渲染視圖中是HostText就可以直接復(fù)用。整體代碼如下:
function reconcileSingleTextNode(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
content: string | number
): FiberNode {
// update
if (currentFiber !== null) {
// 類型沒有變,可以復(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)的刪除和移動標(biāo)記后,在completeWork主要是做更新的標(biāo)記。
對于單一的節(jié)點來說,更新標(biāo)記分為2種,
- 第一種是文本元素的更新,主要是新舊文本內(nèi)容的不一樣。
- 第二種是類似div的屬性等更新。這個我們下一節(jié)進(jìn)行講解。
這里我們只對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é)點wip進(jìn)行標(biāo)記。
function markUpdate(fiber: FiberNode) {
fiber.flags |= Update;
}
commitWork流程
通過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
對于文本節(jié)點,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過程中,對于存在要刪除的子節(jié)點,我們會保存在當(dāng)前父節(jié)點的deletions, 所以在刪除階段,我們需要根據(jù)當(dāng)前節(jié)點的deletions屬性進(jìn)行對要刪除的節(jié)點進(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é)點存在要刪除的子節(jié)點的話,我們需要對每一個子節(jié)點進(jìn)行commitDeletion的操作。
commitDeletion
commitDeletion函數(shù)的是對每一個要刪除的子節(jié)點進(jìn)行處理。它的主要功能有幾點:
- 對于不同類型的
fiberNode, 當(dāng)節(jié)點刪除的時候,自身和所有子節(jié)點都需要執(zhí)行的不同的卸載邏輯。例如:函數(shù)組件的useEffect的return函數(shù)執(zhí)行,ref的解綁,class組件的componentUnmount等邏輯處理。 - 由于
fiberNode和dom節(jié)點不是一一對應(yīng)的,所以要找到fiberNode對應(yīng)的dom節(jié)點,然后再執(zhí)行刪除dom節(jié)點的操作。 - 最后將刪除的節(jié)點的
child和return指向刪掉。
基于上面的2點分析,我們很容易就想到,commitDeletion肯定會執(zhí)行DFS向下遍歷,進(jìn)行不同子節(jié)點的刪除邏輯處理。
/**
* rootHostNode 找到對應(yīng)的DOM節(jié)點。
* commitNestedComponent DFS遍歷節(jié)點的進(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中主要是完成我們上面說的2點。
- DFS深度遍歷子節(jié)點
- 找到當(dāng)前要刪除的
fiberNode對應(yīng)的真正的DOM節(jié)點
接受2個參數(shù)。1. 當(dāng)前的fiberNode, 2. 遞歸到不同的子節(jié)點的同時,需要執(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;
}
}
這里可能比較繞,我們下面通過幾個例子總結(jié)一下,這個過程的主要流程。
總結(jié)
如果按照如下的結(jié)構(gòu),要刪除外層div元素,會經(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階段的時候,遍歷deletions數(shù)組。 - 執(zhí)行的div對應(yīng)的
HostComponent, 然后執(zhí)行commitDeletion - 在
commitDeletion中執(zhí)行commitNestedComponent向下DFS遍歷。 - 在遍歷的過程中,每一個節(jié)點都是執(zhí)行一個回調(diào)函數(shù),基于不同的類型執(zhí)行不同的刪除操作,以及記錄我們要刪除的Dom節(jié)點對應(yīng)的fiberNode。
- 所以首先是
div執(zhí)行onCommitUnmount, 由于它是HostComponent,所以將rootHostNode賦值給了div - 向下遞歸到
Child節(jié)點,由于它存在子節(jié)點,繼續(xù)遞歸到child-div節(jié)點,繼續(xù)遍歷到hello world節(jié)點。它不存在子節(jié)點。 - 然后找到
Child的兄弟節(jié)點,以此執(zhí)行,先子后兄。直到回到div節(jié)點。

下一節(jié)預(yù)告
下一節(jié)我們講解通過useState改變狀態(tài)后,如何更新節(jié)點以及函數(shù)組件hooks是如何保存數(shù)據(jù)的。
以上就是React18之update流程從零實現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于React18 update流程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談React + Webpack 構(gòu)建打包優(yōu)化
本篇文章主要介紹了淺談React + Webpack 構(gòu)建打包優(yōu)化,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
React?Native系列之Recyclerlistview使用詳解
這篇文章主要為大家介紹了React?Native系列之Recyclerlistview使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
react項目中redux的調(diào)試工具不起作用的解決
這篇文章主要介紹了react項目中redux的調(diào)試工具不起作用的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
react實現(xiàn)導(dǎo)航欄二級聯(lián)動
這篇文章主要為大家詳細(xì)介紹了react實現(xiàn)導(dǎo)航欄二級聯(lián)動,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03
React Grid Layout基礎(chǔ)使用示例教程
React Grid Layout是一個用于在React應(yīng)用程序中創(chuàng)建可拖拽和可調(diào)整大小的網(wǎng)格布局的庫,通過使用React Grid Layout,我們可以輕松地創(chuàng)建自適應(yīng)的網(wǎng)格布局,并實現(xiàn)拖拽和調(diào)整大小的功能,本文介紹了React Grid Layout的基礎(chǔ)使用方法,感興趣的朋友一起看看吧2024-02-02

