React?diff算法超詳細(xì)講解
上一章中 react 的 render 階段,其中 begin 時(shí)會(huì)調(diào)用 reconcileChildren函數(shù), reconcileChildren 中做的事情就是 react 知名的 diff 過(guò)程,本章會(huì)對(duì) diff 算法進(jìn)行講解。
diff 算法介紹
react 的每次更新,都會(huì)將新的 ReactElement 內(nèi)容與舊的 fiber 樹(shù)作對(duì)比,比較出它們的差異后,構(gòu)建新的 fiber 樹(shù),將差異點(diǎn)放入更新隊(duì)列之中,從而對(duì)真實(shí) dom 進(jìn)行 render。簡(jiǎn)單來(lái)說(shuō)就是如何通過(guò)最小代價(jià)將舊的 fiber 樹(shù)轉(zhuǎn)換為新的 fiber 樹(shù)。
經(jīng)典的 diff 算法 中,將一棵樹(shù)轉(zhuǎn)為另一棵樹(shù)的最低時(shí)間復(fù)雜度為 O(n^3),其中 n 為樹(shù)種節(jié)點(diǎn)的個(gè)數(shù)。假如采用這種 diff 算法,一個(gè)應(yīng)用有 1000 個(gè)節(jié)點(diǎn)的情況下,需要比較 十億 次才能將 dom 樹(shù)更新完成,顯然這個(gè)性能是無(wú)法讓人接受的。
因此,想要將 diff 應(yīng)用于 virtual dom 中,必須實(shí)現(xiàn)一種高效的 diff 算法。React 便通過(guò)制定了一套大膽的策略,實(shí)現(xiàn)了 O(n) 的時(shí)間復(fù)雜度更新 virtual dom。
diff 策略
react 將 diff 算法優(yōu)化到 O(n) 的時(shí)間復(fù)雜度,基于了以下三個(gè)前提策略:
- 只對(duì)同級(jí)元素進(jìn)行比較。Web UI 中 DOM 節(jié)點(diǎn)跨層級(jí)的移動(dòng)操作特別少,可以忽略不計(jì),如果出現(xiàn)跨層級(jí)的 dom 節(jié)點(diǎn)更新,則不進(jìn)行復(fù)用。
- 兩個(gè)不同類型的組件會(huì)產(chǎn)生兩棵不同的樹(shù)形結(jié)構(gòu)。
- 對(duì)同一層級(jí)的子節(jié)點(diǎn),開(kāi)發(fā)者可以通過(guò)
key來(lái)確定哪些子元素可以在不同渲染中保持穩(wěn)定。
上面的三種 diff 策略,分別對(duì)應(yīng)著 tree diff、component diff 和 element diff。
tree diff
根據(jù)策略一,react 會(huì)對(duì) fiber 樹(shù)進(jìn)行分層比較,只比較同級(jí)元素。這里的同級(jí)指的是同一個(gè)父節(jié)點(diǎn)下的子節(jié)點(diǎn)(往上的祖先節(jié)點(diǎn)也都是同一個(gè)),而不是樹(shù)的深度相同。
如上圖所示,react 的 tree diff 是采用深度優(yōu)先遍歷,所以要比較的元素向上的祖先元素都會(huì)一致,即圖中會(huì)對(duì)相同顏色的方框內(nèi)圈出的元素進(jìn)行比較,例如左邊樹(shù)的 A 節(jié)點(diǎn)下的子節(jié)點(diǎn) C、D 會(huì)與右邊樹(shù) A 節(jié)點(diǎn)下的 C、D、E進(jìn)行比較。
當(dāng)元素出現(xiàn)跨層級(jí)的移動(dòng)時(shí),例如下圖:

A 子樹(shù)從 root 節(jié)點(diǎn)下到了 B 節(jié)點(diǎn)下,在 react diff 過(guò)程中并不會(huì)直接將 A 子樹(shù)移動(dòng)到 B 子樹(shù)下,而是進(jìn)行如下操作:
- 在 root 節(jié)點(diǎn)下刪除 A 節(jié)點(diǎn)
- 在 B 節(jié)點(diǎn)下創(chuàng)建 A 子節(jié)點(diǎn)
- 在新創(chuàng)建的 A 子節(jié)點(diǎn)下創(chuàng)建 C、D 節(jié)點(diǎn)
component diff
對(duì)于組件之間的比較,只要它們的類型不同,就判斷為它們是兩棵不同的樹(shù)形結(jié)構(gòu),直接會(huì)將它們給替換掉。
例如下面的兩棵樹(shù),左邊樹(shù) B 節(jié)點(diǎn)和右邊樹(shù) K 節(jié)點(diǎn)除了類型不同(比如 B 為 div 類型,K 為 p 類型),內(nèi)容完全一致,但 react 依然后直接替換掉整個(gè)節(jié)點(diǎn)。實(shí)際經(jīng)過(guò)的變換是:
- 在 root 節(jié)點(diǎn)下創(chuàng)建 K 節(jié)點(diǎn)
- 在 K 節(jié)點(diǎn)下創(chuàng)建 E、F 節(jié)點(diǎn)
- 在 F 節(jié)點(diǎn)下創(chuàng)建 G、H 節(jié)點(diǎn)
- 在 root 節(jié)點(diǎn)下刪除 B 子節(jié)點(diǎn)

雖然如果在本例中改變類型復(fù)用子元素性能會(huì)更高一點(diǎn),但是在時(shí)機(jī)應(yīng)用開(kāi)發(fā)中類型不一致子內(nèi)容完全一致的情況極少,對(duì)這種情況過(guò)多判斷反而會(huì)增加時(shí)機(jī)復(fù)雜度,降低平均性能。
element diff
react 對(duì)于同層級(jí)的元素進(jìn)行比較時(shí),會(huì)通過(guò) key 對(duì)元素進(jìn)行比較以識(shí)別哪些元素可以穩(wěn)定的渲染。同級(jí)元素的比較存在插入、刪除和移動(dòng)三種操作。
如下圖左邊的樹(shù)想要轉(zhuǎn)變?yōu)橛疫叺臉?shù):

實(shí)際經(jīng)過(guò)的變換如下:
- 將 root 節(jié)點(diǎn)下 A 子節(jié)點(diǎn)移動(dòng)至 B 子節(jié)點(diǎn)之后
- 在 root 節(jié)點(diǎn)下新增 E 子節(jié)點(diǎn)
- 將 root 節(jié)點(diǎn)下 C 子節(jié)點(diǎn)刪除

結(jié)合源碼看 diff
整體流程
diff 算法從 reconcileChildren 函數(shù)開(kāi)始,根據(jù)當(dāng)前 fiber 是否存在,決定是直接渲染新的 ReactElement 內(nèi)容還是與當(dāng)前 fiber 去進(jìn)行 Diff,相關(guān)參考視頻講解:傳送門(mén)
export function reconcileChildren(
current: Fiber | null, // 當(dāng)前 fiber 節(jié)點(diǎn)
workInProgress: Fiber, // 父 fiber
nextChildren: any, // 新生成的 ReactElement 內(nèi)容
renderLanes: Lanes, // 渲染的優(yōu)先級(jí)
) {
if (current === null) {
// 如果當(dāng)前 fiber 節(jié)點(diǎn)為空,則直接將新的 ReactElement 內(nèi)容生成新的 fiber
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 當(dāng)前 fiber 節(jié)點(diǎn)不為空,則與新生成的 ReactElement 內(nèi)容進(jìn)行 diff
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
因?yàn)槲覀冎饕且獙W(xué)習(xí) diff 算法,所以我們暫時(shí)先不關(guān)心 mountChildFibers 函數(shù),主要關(guān)注 reconcileChildFibers ,我們來(lái)看一下它的源碼:
function reconcileChildFibers(
returnFiber: Fiber, // 父 Fiber
currentFirstChild: Fiber | null, // 父 fiber 下要對(duì)比的第一個(gè)子 fiber
newChild: any, // 更新后的 React.Element 內(nèi)容
lanes: Lanes, // 更新的優(yōu)先級(jí)
): Fiber | null {
// 對(duì)新創(chuàng)建的 ReactElement 最外層是 fragment 類型單獨(dú)處理,比較其 children
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 對(duì)更新后的 React.Element 是單節(jié)點(diǎn)的處理
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
// 常規(guī) react 元素
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// react.portal 類型
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// react.lazy 類型
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
}
// 更新后的 React.Element 是多節(jié)點(diǎn)的處理
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 迭代器函數(shù)的單獨(dú)處理
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// 純文本節(jié)點(diǎn)的類型處理
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
// 不符合以上情況都視為 empty,直接從父節(jié)點(diǎn)刪除所有舊的子 Fiber
return deleteRemainingChildren(returnFiber, currentFirstChild);
}入口函數(shù)中,接收 returnFiber、currentFirstChild、newChild、lanes 四個(gè)參數(shù),其中,根據(jù) newChid 的類型,我們主要關(guān)注幾個(gè)比較常見(jiàn)的類型的 diff,單 React 元素的 diff、純文本類型的 diff 和 數(shù)組類型的 diff。
所以根據(jù) ReactElement 類型走的不同流程如下:

新內(nèi)容為 REACT_ELEMENT_TYPE
當(dāng)新創(chuàng)建的節(jié)點(diǎn) type 為 object 時(shí),我們看一下其為 REACT_ELEMENT_TYPE 類型的 diff,即 placeSingleChild(reconcileSingleElement(...)) 函數(shù)。
先看一下 reconcileSingleElement 函數(shù)的源碼:
function reconcileSingleElement(
returnFiber: Fiber, // 父 fiber
currentFirstChild: Fiber | null, // 父 fiber 下第一個(gè)開(kāi)始對(duì)比的舊的子 fiber
element: ReactElement, // 當(dāng)前的 ReactElement內(nèi)容
lanes: Lanes, // 更新的優(yōu)先級(jí)
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 處理舊的 fiber 由多個(gè)節(jié)點(diǎn)變成新的 fiber 一個(gè)節(jié)點(diǎn)的情況
// 循環(huán)遍歷父 fiber 下的舊的子 fiber,直至遍歷完或者找到 key 和 type 都與新節(jié)點(diǎn)相同的情況
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 如果新的 ReactElement 和舊 Fiber 都是 fragment 類型且 key 相等
// 對(duì)舊 fiber 后面的所有兄弟節(jié)點(diǎn)添加 Deletion 副作用標(biāo)記,用于 dom 更新時(shí)刪除
deleteRemainingChildren(returnFiber, child.sibling);
// 通過(guò) useFiber, 基于舊的 fiber 和新的 props.children,克隆生成一個(gè)新的 fiber,新 fiber 的 index 為 0,sibling 為 null
// 這便是所謂的 fiber 復(fù)用
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
} else {
if (
// 如果新的 ReactElement 和舊 Fiber 的 key 和 type 都相等
child.elementType === elementType ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
(enableLazyElements &&
typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 對(duì)舊 fiber 后面的所有兄弟節(jié)點(diǎn)添加 Deletion 副作用標(biāo)記,用于 dom 更新時(shí)刪除
deleteRemainingChildren(returnFiber, child.sibling);
// 通過(guò) useFiber 復(fù)用新節(jié)點(diǎn)并返回
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// 若 key 相同但是 type 不同說(shuō)明不匹配,移除舊 fiber 及其后面的兄弟 fiber
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 若 key 不同,對(duì)當(dāng)前的舊 fiber 添加 Deletion 副作用標(biāo)記,繼續(xù)對(duì)其兄弟節(jié)點(diǎn)遍歷
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 都遍歷完之后說(shuō)明沒(méi)有匹配到 key 和 type 都相同的 fiber
if (element.type === REACT_FRAGMENT_TYPE) {
// 如果新節(jié)點(diǎn)是 fragment 類型,createFiberFromFragment 創(chuàng)建新的 fragment 類型 fiber并返回
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
// createFiberFromElement 創(chuàng)建 fiber 并返回
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}根據(jù)源碼我們可以得知,reconcileSingleElement 函數(shù)中,會(huì)遍歷父 fiber 下所有的舊的子 fiber,尋找與新生成的 ReactElement 內(nèi)容的 key 和 type 都相同的子 fiber。每次遍歷對(duì)比的過(guò)程中:
- 若當(dāng)前舊的子 fiber 與新內(nèi)容 key 或 type 不一致,對(duì)當(dāng)前舊的子 fiber 添加
Deletion副作用標(biāo)記(用于 dom 更新時(shí)刪除),繼續(xù)對(duì)比下一個(gè)舊子 fiber - 若當(dāng)前舊的子 fiber 與新內(nèi)容 key 或 type 一致,則判斷為可復(fù)用,通過(guò)
deleteRemainingChildren對(duì)該子 fiber 后面所有的兄弟 fiber 添加Deletion副作用標(biāo)記,然后通過(guò)useFiber基于該子 fiber 和新內(nèi)容的 props 生成新的 fiber 進(jìn)行復(fù)用,結(jié)束遍歷。
若都遍歷完沒(méi)找到與新內(nèi)容 key 或 type 子 fiber,此時(shí)父 fiber 下的所有舊的子 fiber 都已經(jīng)添加了 Deletion 副作用標(biāo)記,通過(guò) createFiberFromElement 基于新內(nèi)容創(chuàng)建新的 fiber 并將其 return指向父 fiber。
再來(lái)看 placeSingleChild 的源碼:
function placeSingleChild(newFiber: Fiber): Fiber {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
placeSingleChild 中做的事情更為簡(jiǎn)單,就是將 reconcileSingleElement 中生成的新 fiber 打上 Placement 的標(biāo)記,表示 dom 更新渲染時(shí)要進(jìn)行插入。
新內(nèi)容為純文本類型
當(dāng)新創(chuàng)建節(jié)點(diǎn)的 typeof 為 string 或者 number 時(shí),表示是純文本節(jié)點(diǎn),使用 placeSingleChild(reconcileSingleTextNode(...)) 函數(shù)進(jìn)行 diff。
placeSingleChild 前面說(shuō)過(guò)了,我們主要看 reconcileSingleTextNode 的源碼:
function reconcileSingleTextNode(
returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, lanes: Lanes,
): Fiber {
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// deleteRemainingChildren 對(duì)舊 fiber 后面的所有兄弟節(jié)點(diǎn)添加 Deletion 副作用標(biāo)記,用于 dom 更新時(shí)刪除
// useFiber 傳入 textContext 復(fù)用當(dāng)前 fiber
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// 若未匹配到,createFiberFromText 創(chuàng)建新的 fiber
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
新內(nèi)容為純文本時(shí) diff 比較簡(jiǎn)單,只需要判斷當(dāng)前父 fiber 的第一個(gè)舊子 fiber 類型:
- 當(dāng)前 fiber 也為文本類型的節(jié)點(diǎn)時(shí),
deleteRemainingChildren對(duì)第一個(gè)舊子 fiber 的所有兄弟 fiber 添加Deletion副作用標(biāo)記,然后通過(guò)useFiber基于當(dāng)前 fiber 和 textContent 創(chuàng)建新的 fiber 復(fù)用,將其 return 指向父 fiber - 否則通過(guò)
deleteRemainingChildren對(duì)所有舊的子 fiber 添加Deletion副作用標(biāo)記,然后createFiberFromText創(chuàng)建新的文本類型 fiber 節(jié)點(diǎn),將其 return 指向父 fiber
所以對(duì)文本類型 diff 的流程如下:

新內(nèi)容為數(shù)組類型
上面所說(shuō)的兩種情況,都是一個(gè)或多個(gè)子 fiebr 變成單個(gè) fiber。新內(nèi)容為數(shù)組類型時(shí),意味著要將一個(gè)或多個(gè)子 fiber 替換為多個(gè) fiber,內(nèi)容相對(duì)復(fù)雜,我們看一下 reconcileChildrenArray 的源碼:
function reconcileChildrenArray(
returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, lanes: Lanes,
): Fiber | null {
// 開(kāi)發(fā)環(huán)境下會(huì)校驗(yàn) key 是否存在且合法,否則會(huì)報(bào) warning
if (__DEV__) {
let knownKeys = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
}
let resultingFirstChild: Fiber | null = null; // 最終要返回的第一個(gè)子 fiber
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 因?yàn)樵趯?shí)際的應(yīng)用開(kāi)發(fā)中,react 發(fā)現(xiàn)更新的情況遠(yuǎn)大于新增和刪除的情況,所以這里優(yōu)先處理更新
// 根據(jù) oldFiber 的 index 和 newChildren 的下標(biāo),找到要對(duì)比更新的 oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 通過(guò) updateSlot 來(lái) diff oldFiber 和新的 child,生成新的 Fiber
// updateSlot 與上面兩種類型的 diff 類似,如果 oldFiber 可復(fù)用,則根據(jù) oldFiber 和 child 的 props 生成新的 fiber;否則返回 null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber 為 null 說(shuō)明不可復(fù)用,退出第一輪的循環(huán)
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 記錄復(fù)用的 oldFiber 的 index,同時(shí)給新 fiber 打上 Placement 副作用標(biāo)簽
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 如果上一個(gè) newFiber 為 null,說(shuō)明這是第一個(gè)生成的 newFiber,設(shè)置為 resultingFirstChild
resultingFirstChild = newFiber;
} else {
// 否則構(gòu)建鏈?zhǔn)疥P(guān)系
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// newChildren遍歷完了,說(shuō)明剩下的 oldFiber 都是待刪除的 Fiber
// 對(duì)剩下 oldFiber 標(biāo)記 Deletion
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// olderFiber 遍歷完了
// newChildren 剩下的節(jié)點(diǎn)都是需要新增的節(jié)點(diǎn)
for (; newIdx < newChildren.length; newIdx++) {
// 遍歷剩下的 child,通過(guò) createChild 創(chuàng)建新的 fiber
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 處理dom移動(dòng),// 記錄 index,同時(shí)給新 fiber 打上 Placement 副作用標(biāo)簽
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新創(chuàng)建 fiber 加入到 fiber 鏈表樹(shù)中
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// oldFiber 和 newChildren 都未遍歷完
// mapRemainingChildren 生成一個(gè)以 oldFiber 的 key 為 key, oldFiber 為 value 的 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 對(duì)剩下的 newChildren 進(jìn)行遍歷
for (; newIdx < newChildren.length; newIdx++) {
// 找到 mapRemainingChildren 中 key 相等的 fiber, 創(chuàng)建新 fiber 復(fù)用
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 刪除當(dāng)前找到的 fiber
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 處理dom移動(dòng),記錄 index,同時(shí)給新 fiber 打上 Placement 副作用標(biāo)簽
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新創(chuàng)建 fiber 加入到 fiber 鏈表樹(shù)中
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 剩余的舊 fiber 的打上 Deletion 副作用標(biāo)簽
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}從上述代碼我們可以得知,對(duì)于新增內(nèi)容為數(shù)組時(shí),react 會(huì)對(duì)舊 fiber 和 newChildren 進(jìn)行遍歷。
首先先對(duì) newChildren 進(jìn)行第一輪遍歷,將當(dāng)前的 oldFiber 與 當(dāng)前 newIdx 下標(biāo)的 newChild 通過(guò) updateSlot 進(jìn)行 diff,diff 的流程和上面單節(jié)點(diǎn)的 diff 類似,然后返回 diff 后的結(jié)果:
- 如果 diff 后 oldFiber 和 newIdx 的 key 和 type 一致,說(shuō)明可復(fù)用。根據(jù) oldFiber 和 newChild 的 props 生成新的 fiber,通過(guò)
placeChild給新生成的 fiber 打上Placement副作用標(biāo)記,同時(shí)新 fiber 與之前遍歷生成的新 fiber 構(gòu)建鏈表樹(shù)關(guān)系。然后繼續(xù)執(zhí)行遍歷,對(duì)下一個(gè) oldFiber 和下一個(gè) newIdx 下標(biāo)的 newFiber 繼續(xù) diff - 如果 diff 后 oldFiber 和 newIdx 的 key 或 type 不一致,那么說(shuō)明不可復(fù)用,返回的結(jié)果為 null,第一輪遍歷結(jié)束
第一輪遍歷結(jié)束后,可能會(huì)執(zhí)行以下幾種情況:
- 若 newChildren 遍歷完了,那剩下的 oldFiber 都是待刪除的,通過(guò)
deleteRemainingChildren對(duì)剩下的 oldFiber 打上Deletion副作用標(biāo)記 - 若 oldFiber 遍歷完了,那剩下的 newChildren 都是需要新增的,遍歷剩下的 newChildren,通過(guò)
createChild創(chuàng)建新的 fiber,placeChild給新生成的 fiber 打上Placement副作用標(biāo)記并添加到 fiber 鏈表樹(shù)中。 - 若 oldFiber 和 newChildren 都未遍歷完,通過(guò)
mapRemainingChildren創(chuàng)建一個(gè)以剩下的 oldFiber 的 key 為 key,oldFiber 為 value 的 map。然后對(duì)剩下的 newChildren 進(jìn)行遍歷,通過(guò)updateFromMap在 map 中尋找具有相同 key 創(chuàng)建新的fiber(若找到則基于 oldFiber 和 newChild 的 props創(chuàng)建,否則直接基于 newChild 創(chuàng)建),則從 map 中刪除當(dāng)前的 key,然后placeChild給新生成的 fiber 打上Placement副作用標(biāo)記并添加到 fiber 鏈表樹(shù)中。遍歷完之后則 existingChildren 還剩下 oldFiber 的話,則都是待刪除的 fiber,deleteChild對(duì)其打上Deletion副作用標(biāo)記。
diff 后的渲染
diff 流程結(jié)束后,會(huì)形成新的 fiber 鏈表樹(shù),鏈表樹(shù)上的 fiber 通過(guò) flags 字段做了副作用標(biāo)記,主要有以下幾種:
- Deletion:會(huì)在渲染階段對(duì)對(duì)應(yīng)的 dom 做刪除操作
- Update:在 fiber.updateQueue 上保存了要更新的屬性,在渲染階段會(huì)對(duì) dom 做更新操作
- Placement:Placement 可能是插入也可能是移動(dòng),實(shí)際上兩種都是插入動(dòng)作。react 在更新時(shí)會(huì)優(yōu)先去尋找要插入的 fiber 的 sibling,如果找到了執(zhí)行 dom 的
insertBefore方法,如果沒(méi)有找到就執(zhí)行 dom 的appendChild方法,從而實(shí)現(xiàn)了新節(jié)點(diǎn)插入位置的準(zhǔn)確性
在 completeUnitWork 階段結(jié)束后,react 會(huì)根據(jù) fiber 鏈表樹(shù)的 flags,構(gòu)建一個(gè) effectList 鏈表,里面記錄了哪些 fiber 需要進(jìn)行插入、刪除、更新操作,在后面的 commit 階段進(jìn)行真實(shí) dom 節(jié)點(diǎn)的更新,下一章將詳細(xì)講述 commit 階段。
到此這篇關(guān)于React diff算法超詳細(xì)講解的文章就介紹到這了,更多相關(guān)React diff算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React-native橋接Android原生開(kāi)發(fā)詳解
本篇文章主要介紹了React-native橋接Android原生開(kāi)發(fā)詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
react-native動(dòng)態(tài)切換tab組件的方法
在APP中免不了要使用tab組件,有的是tab切換,也有的是tab分類切換.這篇文章主要介紹了react-native動(dòng)態(tài)切換tab組件的方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-07-07
詳解React開(kāi)發(fā)必不可少的eslint配置
本篇文章主要介紹了詳解React開(kāi)發(fā)必不可少的eslint配置,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
詳解如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤
在當(dāng)前的Web開(kāi)發(fā)中,使用React構(gòu)建單頁(yè)面應(yīng)用(SPA)已經(jīng)成為一種常見(jiàn)的做法,然而,當(dāng)應(yīng)用程序遇到錯(cuò)誤時(shí),有可能會(huì)導(dǎo)致整個(gè)頁(yè)面崩潰,給用戶帶來(lái)不好的體驗(yàn),本文將介紹如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤,以防止整個(gè)頁(yè)面的崩潰,需要的朋友可以參考下2023-09-09
react中代碼塊輸出,代碼高亮顯示,帶行號(hào),能復(fù)制的問(wèn)題
這篇文章主要介紹了react中代碼塊輸出,代碼高亮顯示,帶行號(hào),能復(fù)制的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
webpack手動(dòng)配置React開(kāi)發(fā)環(huán)境的步驟
本篇文章主要介紹了webpack手動(dòng)配置React開(kāi)發(fā)環(huán)境的步驟,webpack手動(dòng)配置一個(gè)獨(dú)立的React開(kāi)發(fā)環(huán)境, 開(kāi)發(fā)環(huán)境完成后, 支持自動(dòng)構(gòu)建, 自動(dòng)刷新, sass語(yǔ)法 等功能...感興趣的小伙伴們可以參考一下2018-07-07
React如何使用錯(cuò)誤邊界(Error Boundaries)捕獲組件錯(cuò)誤
在 React 里,錯(cuò)誤邊界就像是一個(gè)“小衛(wèi)士”,專門(mén)負(fù)責(zé)在組件出現(xiàn)錯(cuò)誤時(shí)挺身而出,避免整個(gè)應(yīng)用因?yàn)橐粋€(gè)小錯(cuò)誤就崩潰掉,下面小編就來(lái)為大家介紹一下如何利用它捕獲組件錯(cuò)誤吧2025-03-03
webpack 2.x配置reactjs基本開(kāi)發(fā)環(huán)境詳解
本篇文章主要介紹了webpack 2.x配置reactjs基本開(kāi)發(fā)環(huán)境詳解,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08

