深入理解React Diff算法

來源 | https://segmentfault.com/a/1190000039021724
源碼結(jié)構(gòu)
打上effectTag可以標識這個fiber發(fā)生了怎樣的變化,例如:新增(Placement)、更新(Update)、刪除(Deletion),這些被打上flag的fiber會在complete階段被收集起來,形成一個effectList鏈表,只包含這些需要操作的fiber,最后在commit階段被更新掉。
function updateClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {...// 計算狀態(tài)shouldUpdate = updateClassInstance(current,workInProgress,Component,nextProps,renderLanes,);...// 執(zhí)行render,進入diff,為fiber打上effectTagconst nextUnitOfWork = finishClassComponent(current,workInProgress,Component,shouldUpdate,hasContext,renderLanes,);return nextUnitOfWork;}
在finishClassComponent函數(shù)中,調(diào)用reconcileChildFibers去做diff,而reconcileChildFibers實際上就是ChildReconciler,這是diff的核心函數(shù),
該函數(shù)針對組件render生成的新節(jié)點的類型,調(diào)用不同的函數(shù)進行處理。
function ChildReconciler(shouldTrackSideEffects) {...function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes,): Fiber {// 單節(jié)點diff}function reconcileChildrenArray(returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<*>,lanes: Lanes,): Fiber | null {// 多節(jié)點diff}...function reconcileChildFibers(returnFiber: Fiber,currentFirstChild: Fiber | null,newChild: any, lanes: Lanes,): Fiber | null {const isObject = typeof newChild === 'object' && newChild !== null;if (isObject) {// 處理單節(jié)點switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE:return placeSingleChild(reconcileSingleElement(returnFiber,currentFirstChild,newChild,lanes,),);case REACT_PORTAL_TYPE:...case REACT_LAZY_TYPE:...}}if (typeof newChild === 'string' || typeof newChild === 'number') {// 處理文本節(jié)點}if (isArray(newChild)) {// 處理多節(jié)點return reconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);}...}return reconcileChildFibers;}
Diff的主體
關于Diff的參與者,在reconcileChildren函數(shù)的入?yún)⒅锌梢钥闯?/span>
workInProgress.child = reconcileChildFibers(workInProgress,current.child,nextChildren,renderLanes,);
workInProgress:作為父節(jié)點傳入,新生成的第一個fiber的return會被指向它。
current.child:舊fiber節(jié)點,diff生成新fiber節(jié)點時會用新生成的ReactElement和它作比較。
nextChildren:新生成的ReactElement,會以它為標準生成新的fiber節(jié)點。
renderLanes:本次的渲染優(yōu)先級,最終會被掛載到新fiber的lanes屬性上。
可以看出,diff的兩個主體是:oldFiber(current.child)和newChildren(nextChildren,新的ReactElement),它們是兩個不一樣的數(shù)據(jù)結(jié)構(gòu)。
比如現(xiàn)在有組件
current樹中fiber | | A --sibling---> B --sibling---> C
current fiber 對應的組件render的結(jié)果[{$$typeof: Symbol(react.element), type: "div", key: "A" },{$$typeof: Symbol(react.element), type: "div", key: "B" },{$$typeof: Symbol(react.element), type: "div", key: "B" },]
Diff的基本原則
對于新舊兩種結(jié)構(gòu)來說,場景有節(jié)點自身更新、節(jié)點增刪、節(jié)點移動三種情況。面對復雜的情況,即使最前沿的算法,復雜度也極高。面對這種情況,React以如下策略應對:
即使兩個元素的子樹完全一樣,但前后的父級元素不同,依照規(guī)則div元素及其子樹會完全銷毀,并重建一個p元素及其子樹,不會嘗試復用子樹。
舊<div><span>aspan><span>bspan>div>新<p><span>aspan><span>bspan>p>
使用tag(標簽名)和 key識別節(jié)點,區(qū)分出前后的節(jié)點是否變化,以達到盡量復用無變化的節(jié)點。
舊<p key="a">aap><h1 key="b">bbh1>新<h1 key="b">bbh1><p key="a">aap>
因為tag 和 key的存在,所以React可以知道這兩個節(jié)點只是位置發(fā)生了變化。
場景
上面說到diff算法應對三種場景:節(jié)點更新、節(jié)點增刪、節(jié)點移動,但一個fiber的子元素有可能是單節(jié)點,也有可能是多節(jié)點。所以依據(jù)這兩類節(jié)點可以再細分為:
單節(jié)點更新、單節(jié)點增刪。
多節(jié)點更新、多節(jié)點增刪、多節(jié)點移動。
什么是節(jié)點的更新呢?對于DOM節(jié)點來說,在前后的節(jié)點類型(tag)和key都相同的情況下,節(jié)點的屬性發(fā)生了變化,是節(jié)點更新。若前后的節(jié)點tag或者key不相同,Diff算法會認為新節(jié)點和舊節(jié)點毫無關系。
以下例子中,key為b的新節(jié)點的className發(fā)生了變化,是節(jié)點更新。
舊<div className={'a'} key={'a'}>aadiv><div className={'b'} key={'b'}>bbdiv>新<div className={'a'} key={'a'}>aadiv><div className={'bcd'} key={'b'}>bbdiv>
以下例子中,新節(jié)點的className雖然有變化,但key也變化了,不屬于節(jié)點更新
舊<div className={'a'} key={'a'}>aadiv><div className={'b'} key={'b'}>bbdiv>新<div className={'a'} key={'a'}>aadiv><div className={'bcd'} key={'bbb'}>bbdiv>
以下例子中,新節(jié)點的className雖然有變化,但tag也變化了,不屬于節(jié)點更新
舊<div className={'a'} key={'a'}>aadiv><div className={'b'} key={'b'}>bbdiv>新<div className={'a'} key={'a'}>aadiv><p className={'bcd'} key={'b'}>bbp>
下面來分開敘述一下單節(jié)點和多節(jié)點它們各自的更新策略。
單節(jié)點
若組件產(chǎn)出的元素是如下的類型:
<div key="a">aadiv>
那么它最終產(chǎn)出的ReactElement為下面這樣(省略了一些與diff相關度不大的屬性)
{$typeof: Symbol(react.element), type: "div", key: "a"...}
單節(jié)點指newChildren為單一節(jié)點,但是oldFiber的數(shù)量不一定,所以實際有如下三種場景:
為了降低理解成本,我們用簡化的節(jié)點模型來說明問題,字母代表key。
單個舊節(jié)點
舊:A新:A
多個舊節(jié)點
舊:A - B - C新:B
沒有舊節(jié)點
舊:--新:A
對于單節(jié)點的diff,其實就只有更新操作,不會涉及位移和位置的變化,單節(jié)點的更新會調(diào)用reconcileSingleElement函數(shù)處理。該函數(shù)中對以上三種場景都做了覆蓋。但實際上面的情況對于React來說只是兩種,oldFiber鏈是否為空。因此,在實現(xiàn)上也只處理了這兩種情況。
oldFiber鏈不為空
遍歷它們,找到key相同的節(jié)點,然后刪除剩下的oldFiber節(jié)點,再用匹配的oldFiber,newChildren中新節(jié)點的props來生成新的fiber節(jié)點。
function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes): Fiber {const key = element.key;let child = currentFirstChild;while (child !== null) {if (child.key === key) {switch (child.tag) {case Fragment:...case Block:...default: {if (child.elementType === element.type) {// 先刪除剩下的oldFiber節(jié)點deleteRemainingChildren(returnFiber, child.sibling);// 基于oldFiber節(jié)點和新節(jié)點的props新建新的fiber節(jié)點const existing = useFiber(child, element.props);existing.ref = coerceRef(returnFiber, child, element);existing.return = returnFiber; return existing;}break;}}deleteRemainingChildren(returnFiber, child);break;} else {// 沒匹配到說明新的fiber節(jié)點無法從oldFiber節(jié)點新建// 刪除掉所有oldFiber節(jié)點deleteChild(returnFiber, child);}child = child.sibling;}...}
oldFiber鏈為空
對于沒有oldFiber節(jié)點的情況,只能新建newFiber節(jié)點。邏輯不復雜。
function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes): Fiber {const key = element.key;let child = currentFirstChild;while (child !== null) {// oldFiber鏈非空的處理...} if (element.type === REACT_FRAGMENT_TYPE) {// 處理Fragment類型的節(jié)點...} else {// 用產(chǎn)生的ReactElement新建一個fiber節(jié)點const created = createFiberFromElement(element, returnFiber.mode, lanes);created.ref = coerceRef(returnFiber, currentFirstChild, element);created.return = returnFiber;return created;}}
單節(jié)點的更新就是這樣的處理,真正比較復雜的情況是多節(jié)點的diff。因為它涉及到節(jié)點的增刪和位移。
多節(jié)點
若組件最終產(chǎn)出的DOM元素是如下這樣:
<div key="a">aadiv><div key="b">bbdiv><div key="c">ccdiv><div key="d">dddiv>
那么最終的newChildren為下面這樣(省略了一些與diff相關度不大的屬性)
[{$$typeof: Symbol(react.element), type: "div", key: "a" },{$$typeof: Symbol(react.element), type: "div", key: "b" },{$$typeof: Symbol(react.element), type: "div", key: "c" },{$$typeof: Symbol(react.element), type: "div", key: "d" }]
多節(jié)點的變化有以下四種可能性。
節(jié)點更新
舊:A - B - C新:`A - B - C`
新增節(jié)點
舊:A - B - C新:A - B - C - `D - E`
刪除節(jié)點
舊:A - B - C - `D - E`新:A - B - C
節(jié)點移動
舊:A - B - C - D - E新:A - B - `D - C - E`
多節(jié)點的情況一定是屬于這四種情況的任意組合,這種情況會調(diào)用reconcileChildrenArray進行diff。按照以上四種情況,它會以newChildren為主體進行最多三輪遍歷,但這三輪遍歷并不是相互獨立的,事實上只有第一輪是從頭開始的,之后的每一輪都是上輪結(jié)束的斷點繼續(xù)。
實際上在平時的實踐中,節(jié)點自身的更新是最多的,所以Diff算法會優(yōu)先處理更新的節(jié)點。因此四輪遍歷又可以按照場景分為兩部分:
第一輪是針對節(jié)點自身屬性更新,剩下的兩輪依次處理節(jié)點的新增、移動,而重點又在移動節(jié)點的處理上,所以本文會著重講解節(jié)點更新和節(jié)點移動的處理,對刪除和新增簡單帶過。
節(jié)點更新
第一輪從頭開始遍歷newChildren,會逐個與oldFiber鏈中的節(jié)點進行比較,判斷節(jié)點的key或者tag是否有變化。
沒變則從oldFiber節(jié)點clone一個props被更新的fiber節(jié)點,新的props來自newChildren中的新節(jié)點,這樣就實現(xiàn)了節(jié)點更新。
有變化說明不滿足復用條件,立即中斷遍歷進入下邊的遍歷。Diff算法的復雜度也因為這個操作大幅降低。
let newIdx = 0;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {...// 更新節(jié)點,對于DOM節(jié)點來說,updateSlot內(nèi)部會判斷// key 和 tag。任意一個不同,則返回nullconst newFiber = updateSlot( returnFiber,oldFiber,newChildren[newIdx],lanes,);// newFiber為null則說明當前的節(jié)點不是更新的場景,中止這一輪循環(huán)if (newFiber === null) {if (oldFiber === null) {oldFiber = nextOldFiber;}break;}...}
我們來看一個例子,假設新舊的節(jié)點如下:
舊:A - B - C - D - E
新:A - B - D - C
在本輪遍歷中,會遍歷A - B - D - C。A和B都是key沒變的節(jié)點,可以直接復用,但當遍歷到D時,發(fā)現(xiàn)key變化了,跳出當前遍歷。例子中A 和 B是自身發(fā)生更新的節(jié)點,后面的D 和 C我們看到它的位置相對于oldFiber鏈發(fā)生了變化,會往下走到處理移動節(jié)點的循環(huán)中。
關于移動節(jié)點的參照物
為了方便說明,把保留在原位的節(jié)點稱為固定節(jié)點。經(jīng)過這次循環(huán)的處理,可以看出固定節(jié)點是A 和 B。在newChildren中,最靠右的固定節(jié)點的位置至關重要,對于后續(xù)的移動節(jié)點的處理來說,它的意義是提供參考位置。所以,每當處理到最后一個固定節(jié)點時,要記住此時它的位置,這個位置就是lastPlacedIndex。關鍵代碼如下:
let newIdx = 0;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {...// 跳出邏輯...// 如果不跳出,記錄最新的固定節(jié)點的位置lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);...}
placeChild方法實際上是移動節(jié)點的方法,但當節(jié)點無需移動的時候,會返回當前節(jié)點的位置,對于固定節(jié)點來說,因為無需移動,所以返回的就是固定節(jié)點的index。
節(jié)點刪除
我們沒有提到對刪除節(jié)點的處理,實際上刪除節(jié)點比較簡單。
舊:A - B - C - D - E
新:A - B - C
因為遍歷的是newChildren,當它遍歷結(jié)束,但oldFiber鏈還沒有遍歷完,那么說明剩下的節(jié)點都要被刪除。直接在oldFiber節(jié)點上標記Deletion的effectTag來實現(xiàn)刪除。
if (newIdx === newChildren.length) {// 新子節(jié)點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除deleteRemainingChildren(returnFiber, oldFiber);return resultingFirstChild;}
deleteRemainingChildren調(diào)用了deleteChild,值得注意的是,刪除不僅僅是標記了effectTag為Deletion,還會將這個被刪除的fiber節(jié)點添加到父級的effectList中。
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {...const last = returnFiber.lastEffect;// 將要刪除的child添加到父級fiber的effectList中,并添加上effectTag為刪除if (last !== null) {last.nextEffect = childToDelete;returnFiber.lastEffect = childToDelete;} else {returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;}childToDelete.nextEffect = null;childToDelete.effectTag = Deletion;}
節(jié)點新增
新增節(jié)點的場景也很好理解,當oldFiber鏈遍歷完,但newChildren還沒遍歷完,那么余下的節(jié)點都屬于新插入的節(jié)點,會新建fiber節(jié)點并以sibling為指針連成fiber鏈。
舊:A - B - C
新:A - B - C - D - E
插入的邏輯(省略了相關度不高的代碼)
if (oldFiber === null) {// 舊的遍歷完了,意味著剩下的都是新增的了for (; newIdx < newChildren.length; newIdx++) { // 首先創(chuàng)建newFiberconst newFiber = createChild(returnFiber, newChildren[newIdx], lanes);...// 再將newFiber連接成以sibling為指針的單向鏈表if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}return resultingFirstChild;}
節(jié)點移動
節(jié)點的移動是如下場景:
舊 A - B -?C - D - E - F
新 A - B -?D - C - E
經(jīng)過第一輪遍歷的處理,固定節(jié)點為A B,最新的固定節(jié)點的位置(lastPlacedIndex)為1(B的位置)。此時oldFiber鏈中還剩C - D - E - F,newChildren中還剩D - C - E。
接下來的邏輯對于位置不一樣的節(jié)點,它自己會先更新再移動。因為此時剩余的節(jié)點位置變了,更新又要復用oldFiber節(jié)點,所以為了在更新時方便查找,會將剩余的oldFiber節(jié)點放入一個以key為鍵,值為oldFiber節(jié)點的map中。稱為existingChildren。
由于newChildren 和 oldFiber節(jié)點都沒遍歷完,說明需要移動位置。此刻需要明確一點,就是這些節(jié)點都在最新的固定節(jié)點的右邊。
移動的邏輯是:newChildren中剩余的節(jié)點,都是不確定要不要移動的,遍歷它們,每一個都去看看這個節(jié)點在oldFiber鏈中的位置(舊位置),遍歷到的節(jié)點有它在newChildren中的位置(新位置):
如果舊位置在lastPlacedIndex的右邊,說明這個節(jié)點位置不變。
原因是舊位置在lastPlacedIndex的右邊,而新節(jié)點的位置也在它的右邊,所以它的位置沒變化。因為位置不變,所以它成了固定節(jié)點,把lastPlacedIndex更新成新位置。
如果舊位置在lastPlacedIndex的左邊,當前這個節(jié)點的位置要往右挪。
原因是舊位置在lastPlacedIndex的左邊,新位置卻在lastPlacedIndex的右邊,所以它要往右挪,但它不是固定節(jié)點。此時無需更新lastPlacedIndex。
我們來用上邊的例子過一下這部分邏輯。
舊 A - B -?C - D - E - F
新 A - B -?D - C - E
位置固定部分 A - B,最右側(cè)的固定節(jié)點為B,lastPlacedIndex為1。這時剩余oldFiber鏈為C - D - E - F,existingChildren為
{C: '節(jié)點C',D: '節(jié)點D',E: '節(jié)點E',F: '節(jié)點F'}
newChildren的剩余部分D - C - E繼續(xù)遍歷。
首先遍歷到D,D在oldFiber鏈中(A - B - C - D - E)的位置為3
3 > 1,oldFiber中D的位置在B的右邊,newChildren中也是如此,所以D的位置不動,此時最新的固定節(jié)點變成了D,更新lastPlacedIndex為3。并從existingChildren中刪除D,
{C: '節(jié)點C',E: '節(jié)點E',F: '節(jié)點F'}
再遍歷到C,C在oldFiber鏈中(A - B - C - D - E)的索引為2
2 < 3,C原來在最新固定節(jié)點(D)的左邊,newChildren中C在D的右邊,所以要給它移動到右邊。并從existingChildren中刪除C。
{E: '節(jié)點E',F: '節(jié)點F'}
再遍歷到E,E在oldFiber鏈中(A - B - C - D - E)的位置為4
4 > 3,oldFiber鏈中E位置在D的位置的右邊,新位置中也是如此,所以E的位置不動,此時最新的固定節(jié)點變成了E,更新lastPlacedIndex為4。并從existingChildren中刪除E,
{F: '節(jié)點F'}
這個時候newChildren都處理完了,針對移動節(jié)點的遍歷結(jié)束。此時還剩一個F節(jié)點,是在oldFiber鏈中的,因為newChildren都處理完了,所以將它刪除即可。
existingChildren.forEach(child => deleteChild(returnFiber, child));
可以看到,節(jié)點的移動是以最右側(cè)的固定節(jié)點位置作為參照的。這些固定節(jié)點是指位置未發(fā)生變化的節(jié)點。每次對比節(jié)點是否需要移動之后,及時更新固定節(jié)點非常重要。
源碼
了解了上邊的多節(jié)點diff原理后,將上邊的關鍵點匹配到源碼上更方便能進一步理解。下面放出帶有詳細注釋的源碼。
function reconcileChildrenArray(returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<*>,lanes: Lanes,): Fiber | null {/* * returnFiber:currentFirstChild的父級fiber節(jié)點* currentFirstChild:當前執(zhí)行更新任務的WIP(fiber)節(jié)點* newChildren:組件的render方法渲染出的新的ReactElement節(jié)點* lanes:優(yōu)先級相關* */// resultingFirstChild是diff之后的新fiber鏈表的第一個fiber。let resultingFirstChild: Fiber | null = null;// resultingFirstChild是新鏈表的第一個fiber。// previousNewFiber用來將后續(xù)的新fiber接到第一個fiber之后let previousNewFiber: Fiber | null = null;// oldFiber節(jié)點,新的child節(jié)點會和它進行比較let oldFiber = currentFirstChild;// 存儲固定節(jié)點的位置let lastPlacedIndex = 0;// 存儲遍歷到的新節(jié)點的索引let newIdx = 0;// 記錄目前遍歷到的oldFiber的下一個節(jié)點let nextOldFiber = null;// 該輪遍歷來處理節(jié)點更新,依據(jù)節(jié)點是否可復用來決定是否中斷遍歷for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {// newChildren遍歷完了,oldFiber鏈沒有遍歷完,此時需要中斷遍歷if (oldFiber.index > newIdx) {nextOldFiber = oldFiber; oldFiber = null;} else {// 用nextOldFiber存儲當前遍歷到的oldFiber的下一個節(jié)點nextOldFiber = oldFiber.sibling;}// 生成新的節(jié)點,判斷key與tag是否相同就在updateSlot中// 對DOM類型的元素來說,key 和 tag都相同才會復用oldFiber// 并返回出去,否則返回nullconst newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes,);// newFiber為 null說明 key 或 tag 不同,節(jié)點不可復用,中斷遍歷if (newFiber === null) {if (oldFiber === null) {// oldFiber 為null說明oldFiber此時也遍歷完了// 是以下場景,D為新增節(jié)點// 舊 A - B - C// 新 A - B - C - D oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {// shouldTrackSideEffects 為true表示是更新過程if (oldFiber && newFiber.alternate === null) {// newFiber.alternate 等同于 oldFiber.alternate// oldFiber為WIP節(jié)點,它的alternate 就是 current節(jié)點// oldFiber存在,并且經(jīng)過更新后的新fiber節(jié)點它還沒有current節(jié)點,// 說明更新后展現(xiàn)在屏幕上不會有current節(jié)點,而更新后WIP// 節(jié)點會稱為current節(jié)點,所以需要刪除已有的WIP節(jié)點deleteChild(returnFiber, oldFiber);}}// 記錄固定節(jié)點的位置lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 將新fiber連接成以sibling為指針的單向鏈表if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;// 將oldFiber節(jié)點指向下一個,與newChildren的遍歷同步移動oldFiber = nextOldFiber;}// 處理節(jié)點刪除。新子節(jié)點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除.if (newIdx === newChildren.length) {// newChildren遍歷結(jié)束,刪除掉oldFiber鏈中的剩下的節(jié)點deleteRemainingChildren(returnFiber, oldFiber);return resultingFirstChild;}// 處理新增節(jié)點。舊的遍歷完了,能復用的都復用了,所以意味著新的都是新插入的了if (oldFiber === null) {for (; newIdx < newChildren.length; newIdx++) {// 基于新生成的ReactElement創(chuàng)建新的Fiber節(jié)點const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);if (newFiber === null) {continue;}// 記錄固定節(jié)點的位置lastPlacedIndexlastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 將新生成的fiber節(jié)點連接成以sibling為指針的單向鏈表if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}return resultingFirstChild;}// 執(zhí)行到這是都沒遍歷完的情況,把剩余的舊子節(jié)點放入一個以key為鍵,值為oldFiber節(jié)點的map中// 這樣在基于oldFiber節(jié)點新建新的fiber節(jié)點時,可以通過key快速地找出oldFiberconst existingChildren = mapRemainingChildren(returnFiber, oldFiber);// 節(jié)點移動for (; newIdx < newChildren.length; newIdx++) {// 基于map中的oldFiber節(jié)點來創(chuàng)建新fiberconst newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, );if (newFiber !== null) {if (shouldTrackSideEffects) {if (newFiber.alternate !== null) {// 因為newChildren中剩余的節(jié)點有可能和oldFiber節(jié)點一樣,只是位置換了,// 但也有可能是是新增的.// 如果newFiber的alternate不為空,則說明newFiber不是新增的。// 也就說明著它是基于map中的oldFiber節(jié)點新建的,意味著oldFiber已經(jīng)被使用了,所以需// 要從map中刪去oldFiberexistingChildren.delete(newFiber.key === null ? newIdx : newFiber.key,);}}// 移動節(jié)點,多節(jié)點diff的核心,這里真正會實現(xiàn)節(jié)點的移動lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 將新fiber連接成以sibling為指針的單向鏈表if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}}if (shouldTrackSideEffects) {// 此時newChildren遍歷完了,該移動的都移動了,那么刪除剩下的oldFiberexistingChildren.forEach(child => deleteChild(returnFiber, child));}return resultingFirstChild;}
總結(jié)
Diff算法通過key和tag來對節(jié)點進行取舍,可直接將復雜的比對攔截掉,然后降級成節(jié)點的移動和增刪這樣比較簡單的操作。
對oldFiber和新的ReactElement節(jié)點的比對,將會生成新的fiber節(jié)點,同時標記上effectTag,這些fiber會被連到workInProgress樹中,作為新的WIP節(jié)點。
樹的結(jié)構(gòu)因此被一點點地確定,而新的workInProgress節(jié)點也基本定型。這意味著,在diff過后,workInProgress節(jié)點的beginWork節(jié)點就完成了。接下來會進入completeWork階段。
點擊進入(地址:https://github.com/neroneroffy/react-source-code-debug)react源碼調(diào)試倉庫

