<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解React Diff算法

          共 17643字,需瀏覽 36分鐘

           ·

          2021-02-01 23:48

          來源 | https://segmentfault.com/a/1190000039021724


          fiber上的updateQueue經(jīng)過React的一番計(jì)算之后,這個(gè)fiber已經(jīng)有了新的狀態(tài),也就是state,對(duì)于類組件來說,state是在render函數(shù)里被使用的,既然已經(jīng)得到了新的state,那么當(dāng)務(wù)之急是執(zhí)行一次render,得到持有新state的ReactElement。
          假設(shè)render一次之后得到了大量的ReactElement,而這些ReactElement之中若只有少量需要更新的節(jié)點(diǎn),那么顯然不能全部去更新它們,此時(shí)就需要有一個(gè)diff過程來決定哪些節(jié)點(diǎn)是真正需要更新的。

          源碼結(jié)構(gòu)

          我們以類組件為例,state的計(jì)算發(fā)生在類組件對(duì)應(yīng)的fiber節(jié)點(diǎn)beginWork中的updateClassInstance函數(shù)中,在狀態(tài)計(jì)算完畢之后,緊跟著就是去調(diào)finishClassComponent執(zhí)行diff、打上effectTag(即新版本的flag)。
          打上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打上effectTag   const 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)在有組件,它計(jì)算完新的狀態(tài)之后,要基于這兩個(gè)東西去做diff,分別是現(xiàn)有fiber樹中(current樹)對(duì)應(yīng)fiber的所有子fiber節(jié)點(diǎn)和的render函數(shù)的執(zhí)行結(jié)果,即那些ReactElements。

          對(duì)應(yīng)fiber的所有子fiber節(jié)點(diǎn):oldFiber

           current樹中  fiber | | A --sibling---> B --sibling---> C

          的render函數(shù)的執(zhí)行結(jié)果,newChildren

           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è)不同,則返回null   const 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)建newFiber    const 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        // 并返回出去,否則返回null        const 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)的位置lastPlacedIndex                lastPlacedIndex = 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快速地找出oldFiber        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);                // 節(jié)點(diǎn)移動(dòng)        for (; newIdx < newChildren.length; newIdx++) {            // 基于map中的oldFiber節(jié)點(diǎn)來創(chuàng)建新fiber            const 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中刪去oldFiber                        existingChildren.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)了,那么刪除剩下的oldFiber           existingChildren.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ù)

          本文完?

          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线免费观看国产黄色片 | 国内激情自拍 | 色护士无遮挡精品 | 男人的天堂网av 男人天堂无码视频 | 人妻互换一二三区免费 |