深入理解React Diff算法

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

