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 啟動(dòng)流程詳細(xì)解析
這篇文章主要介紹了React Native 啟動(dòng)流程簡(jiǎn)析,文以 react-native-cli 創(chuàng)建的示例工程(安卓部分)為例,給大家分析 React Native 的啟動(dòng)流程,需要的朋友可以參考下2021-08-08React創(chuàng)建組件的三種方式及其區(qū)別
本文主要介紹了React創(chuàng)建組件的三種方式及其區(qū)別,具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01react使用CSS實(shí)現(xiàn)react動(dòng)畫(huà)功能示例
這篇文章主要介紹了react使用CSS實(shí)現(xiàn)react動(dòng)畫(huà)功能,結(jié)合實(shí)例形式分析了react使用CSS實(shí)現(xiàn)react動(dòng)畫(huà)功能具體步驟與實(shí)現(xiàn)方法,需要的朋友可以參考下2020-05-05react寫(xiě)一個(gè)select組件的實(shí)現(xiàn)代碼
這篇文章主要介紹了react寫(xiě)一個(gè)select組件的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04