首頁 > 軟體

React Diff演演算法不採用Vue的雙端對比原因詳解

2022-07-07 10:01:42

前言

都說“雙端對比演演算法”,那麼雙端對比演演算法,到底是怎麼樣的呢?跟 React 中的 Diff 演演算法又有什麼不同呢?

要了解這些,我們先了解 React 中的 Diff 演演算法,然後再瞭解 Vue3 中的 Diff 演演算法,最後講一下 Vue2 中的 Diff 演演算法,才能去比較一下他們的區別。

最後講一下為什麼 Vue 中不需要使用 Fiber 架構。

React 官方的解析

其實為什麼 React 不採用 Vue 的雙端對比演演算法,React 官方已經在原始碼的註釋裡已經說明了,我們來看一下 React 官方是怎麼說的。

function reconcileChildrenArray(
returnFiber: Fiber,
 currentFirstChild: Fiber | null,
 newChildren: Array<*>,
 expirationTime: ExpirationTime,
): Fiber | null {
    // This algorithm can't optimize by searching from boths ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.
}

大概的意思就是說:

React 不能通過雙端對比進行 Diff 演演算法優化是因為目前 Fiber 上沒有設定反向連結串列,而且想知道就目前這種方案能持續多久,如果目前這種模式不理想的話,那麼也可以增加雙端對比演演算法。

即使是雙端對比演演算法,我們也要對這種情況進行優化,我們應該使用 Map 這種資料結構方案去替代原來那種幾乎沒有什麼變化也進行暴力比較的方案。它第一次搜尋迴圈是通過 forward-only 這種模式(就是隻從左向右查詢),(第一次迴圈可能還沒有結束,還有節點沒有比對的時候)如果還要繼續向前迴圈查詢那麼就要通過 Map 這種資料型別了。(就目前這個單向連結串列的資料結構,如果採用)雙端對比查詢演演算法比較難控制它反向查詢的,但它確實是一種成功的演演算法。此外,雙端對比演演算法的實現也在我們的工作迭代當中。

第一次迭代,我們就先將就使用這種不好的方案吧,每次新增/移動都要新增所有的資料到一個 Map 的資料型別物件中。

“we'd need to copy the whole set”,這一句,每一個單詞都懂,但就是不知道他想說什麼,所以就不翻譯了,有知道的大神嗎?

本人水平有限,錯漏難免,如有錯漏,懇請各位斧正。

React 的官方雖然解析了,但我們想要徹底理解到底為什麼,還是要去詳細瞭解 React 的 Diff 演演算法是怎麼樣的。在瞭解 React Diff 演演算法之前,我們首先要了解什麼是 Fiber,為什麼 React 中要使用 Fiber?

Fiber 的結構

在 React15 以前 React 的元件更新建立虛擬 DOM 和 Diff 的過程是不可中斷,如果需要更新元件樹層級非常深的話,在 Diff 的過程會非常佔用瀏覽器的執行緒,而我們都知道瀏覽器執行JavaScript 的執行緒和渲染真實 DOM 的執行緒是互斥的,也就是同一時間內,瀏覽器要麼在執行 JavaScript 的程式碼運算,要麼在渲染頁面,如果 JavaScript 的程式碼執行時間過長則會造成頁面卡頓。 基於以上原因 React 團隊在 React16 之後就改寫了整個架構,將原來陣列結構的虛擬DOM,改成叫 Fiber 的一種資料結構,基於這種 Fiber 的資料結構可以實現由原來不可中斷的更新過程變成非同步的可中斷的更新。

Fiber 的資料結構主要長成以下的樣子,主要通過 Fiber 的一些屬性去儲存元件相關的資訊。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作為靜態資料結構的屬性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用於連線其他Fiber節點形成Fiber樹
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作為動態的工作單元的屬性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 排程優先順序相關
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向該fiber在另一次更新時對應的fiber
  this.alternate = null;
}

Fiber 主要靠以下屬性連成一棵樹結構的資料的,也就是 Fiber 連結串列。

// 指向父級Fiber節點
this.return = null;
// 指向子Fiber節點
this.child = null;
// 指向右邊第一個兄弟Fiber節點
this.sibling = null;

舉個例子,如下的元件結構:

function App() {
  return (
    &lt;div&gt;
      i am
      &lt;span&gt;Coboy&lt;/span&gt;
    &lt;/div&gt;
  )
}

對應的 Fiber 連結串列結構:

那麼以上的 Fiber 連結串列的資料結構有什麼特點,就是任何一個位置的 Fiber 節點,都可以非常容易知道它的父 Fiber, 第一個子元素的 Fiber,和它的兄弟節點 Fiber。卻不容易知道它前一個 Fiber 節點是誰,這就是 React 中單向連結串列 Fiber 節點的特點。也正是因為這些即便在協調的過程被中斷了,再恢復協調的時候,依然知道當前的 父節點和孩子節點等資訊。

那麼 React 是將對應元件怎麼生成一個 Fiber 連結串列資料的呢?

Fiber 連結串列的生成

上面的元件在經過 JSX 的編譯之後,初始化的時候會生成成一個類似於 React 15 或者 Vue 那種虛擬 DOM 的資料結構。然後建立一個叫 fiberRoot 的 Fiber 節點,然後開始從 fiberRoot 這個根 Fiber 開始進行協調,生成一棵 Fiber 樹,這個棵樹被稱為:workInProgress Fiber 樹 ,意思是正在工作的 Fiber 樹,接下來我們詳細瞭解一下具體是怎麼生成一棵 Fiber 樹的。要先了解 Fiber 樹的生成原理才更好去理解 Fiber 樹 diff 的過程。

以下是一段簡單版的 Fiber 連結串列生成的程式碼片段。 這個協調子節點的函數接收兩個引數,returnFiber 是父 Fiber,children 是這個節點的子元素的虛擬 DOM資料。

// 這個協調子節點的函數接收兩個引數,returnFiber 是父 Fiber,children 是這個節點的子元素的虛擬 DOM資料。
export function reconcileChildren(returnFiber, children) {
    // 如果是字串或者數位則不建立 Fiber
    if(isStringOrNumber(children)) {
        return
    }
    const newChildren = isArray(children) ? children : [children]
    // 上一輪的 fiber 節點
    let previousNewFiber = null;
    // 初次渲染(false)還是更新(true)
    let shouldTrackSideEffects = !!returnFiber.alternate
    // 老 Fiber 節點
    let oldFiber = returnFiber.alternate && returnFiber.alternate.child
    let nextOldFiber = null
    // 上一次協調返回的位置
    let lastPlacedIndex = 0;
    // 記錄每個 fiber 節點的位置
    let newIdx = 0;
    // 如果不存在老 Fiber 則是初始化的過程,進行 Fiber 連結串列的建立
    if(!oldFiber) {
        for (; newIdx < newChildren.length; newIdx++) {
            // 獲取節點元素內容
            const newChild = newChildren[newIdx]
            // 如果節點為 null 則不需要建立 fiber 節點
            if(newChild === null) {
                continue
            }
            // 建立新 fiber 的時候記錄了關鍵的父 fiber 等重要資訊
            const newFiber = createFiber(newChild, returnFiber)
            // 記錄當前每一個 fiber 的位置
            lastPlacedIndex = placeChild(
                newFiber,
                lastPlacedIndex,
                newIdx,
                shouldTrackSideEffects // 初次渲染(false)還是更新(true)
            )
		    // 當上一輪的 fiber 節點為 null 的時候,這一輪的 fiber 就是頭節點
            if(previousNewFiber === null) {
                // 父 fiber 的 child 就是第一個節點
                returnFiber.child = newFiber
            } else {
                // 如果不是第一個節點,那麼就是兄弟節點
                // 上一輪 fiber 的兄弟節點是這一輪的 fiber 節點
                previousNewFiber.sibling = newFiber;
            }
		   // 記錄上一輪的 fiber,既是這一輪的 fiber 便是下一輪的上一輪 fiber
            previousNewFiber = newFiber
        }
        return
    }
}

構建完的 workInProgress Fiber樹 會在 commit階段 渲染到頁面。

在元件狀態資料發生變更的時候,會根據最新的狀態資料先會生成新的虛擬DOM,再去構建一棵新的 workInProgress Fiber 樹 ,而在重新協調構建新的 Fiber 樹的過程也就是 React Diff 發生的地方。接下來,我們就看看 React Diff 演演算法是怎麼樣的。

React 的 Diff 演演算法

深度優先,有子節點,就遍歷子節點,沒有子節點,就找兄弟節點,沒有兄弟節點,就找叔叔節點,叔叔節點也沒有的話,就繼續往上找,它爺爺的兄弟,如果一直沒找到,就代表所有的更新任務都更新完畢了。

重點是在更新自己的同時需要去協調子節點,也就是傳說中進行 Diff 的地方。

進入協調的時候它自己就是父 Fiber,它的子節點在協調之前,是剛剛通過更新的狀態資料生成的最新的虛擬DOM資料,是個陣列結構的元素資料。

那麼要進行更新,就肯定是以為最新的節點資料為準了,又因為最新的節點資料是一個陣列,所以可以進行迴圈對比每一個節點,很明顯這個迴圈是從左向右進行查詢比對的。

第一輪,常見情況的比對

那麼第一個節點的老 Fiber 怎麼拿到呢?可以通過 父 Fiber 的 child 屬性拿到,這樣第一個節點的老 Fiber 就拿到了,那麼第二節點的老 Fiber,很明顯可以通過第一個節點的老 Fiber 節點的 sibling 屬性拿到,後面的以此類推。

怎麼比對呢?

在迴圈的新節點虛擬DOM資料的時候,拿到新節點虛擬DOM資訊,然後就去和老 Fiber 節點進行比對,如果兩個節點相同則建立一個新的 Fiber 節點並複用一些老 Fiber 節點的資訊,比如真實 DOM,並給這個新的 Fiber 節點打上一個 Update 的標記,代表這個節點需要更新即可。

接著去更新協調位置資訊。

在迴圈的最後進行 Fiber 連結串列的處理:

如果是頭節點,則把新 Fiber 設定為父 Fiber 的 child 屬性的值; 如果不是頭節點,則把新 Fiber 設定為上一輪迴圈的建立的 Fiber 節點的 sibing 屬性的值; 更新上一輪 Fiber 變數的值,就是把這一輪的 Fiber 設定成下一輪的 Fiber; 更新比對的老 Fiber 的值。

如果新節點都能找到能複用的節點,則判斷是否還存在老節點,有則刪除。

第二輪,不常見的情況的比對

如果經過第一輪比對,新節點還存在未比對的,則繼續迴圈查詢。

先將剩下未比對的老 Fiber 節點全部處理成一個 老 Fiber 的 key 或老 Fiber 的 index 為 key,Fiber 節點為 value 的 Map 中,這樣就可以,以 O(1) 複雜度,通過新 Fiber 的 key 去 Map 物件中查詢匹配的 Fiber,找到了,則刪除 Map 物件中的老 Fiber 資料,然後複用匹配到的 Fiber 資料。

接下來,不管有沒有匹配到都進行位置協調,記錄最新的位置資訊,新增的 Fiber 因為沒有存在老 Fiber 而會被打上 Placement 的標記,在將來提交的階段將會被進行新增操作。這個過程跟第一輪最後的處理是一樣的。

在迴圈的最後進行 Fiber 連結串列的處理:

如果是頭節點,則把新 Fiber 設定為父 Fiber 的 child 屬性的值; 如果不是頭節點,則把新 Fiber 設定為上一輪迴圈的建立的 Fiber 節點的 sibing 屬性的值; 更新上一輪 Fiber 變數的值,就是把這一輪的 Fiber 設定成下一輪的 Fiber; 更新比對的老 Fiber 的值。

重點如何協調更新位置資訊

如果是初始渲染,那麼協調位置就只是記錄當前元素下標的位置到 Fiber 節點上。如果是更新階段,就先判斷有沒有老 Fiber 節點,如果沒有老 Fiber 節點,則說明該節點需要建立,就給當前新的 Fiber 節點打上一個 Placement 的標記,如果有老 Fiber 節點,則判斷老 Fiber 節點的位置是否比上一次協調的返回的位置小,如果是,則說明該節點需要移動,給新 Fiber 節點打上一個 Placement 的標記,並繼續返回上一次協調返回的位置;如果老 Fiber 節點的位置大或者等於上一次協調返回的位置,則說明該節點不需要進行位置移動操作,就返回老 Fiber 的位置即可。

這裡需要說明的一點,為什麼移動和新增節點都是 Placement 的標記呢?

因為我們是在協調一個子節點列表,所以不管是新增還是移動都是屬於位置是需要發生變化的,所以新增和移動都是同一種操作情況。

小結

總個來說,React Diff 演演算法分以下幾個步驟:

  • 第一輪,從左向右新老節點進行比對查詢能複用的舊節點,如果有新老節點比對不成功的,則停止這一輪的比對,並記錄了停止的位置。
  • 如果第一輪比對,能把所有的新節點都比對完畢,則刪除舊節點還沒進行比對的節點。
  • 如果第一輪的比對,沒能將所有的新節點都比對完畢,則繼續從第一輪比對停止的位置繼續開始迴圈新節點,拿每一個新節點去老節點裡面進行查詢,有匹配成功的則複用,沒匹配成功的則在協調位置的時候打上 Placement 的標記。
  • 在所有新節點比對完畢之後,檢查還有沒有沒進行復用的舊節點,如果有,則全部刪除。

圖文解釋 React Diff 演演算法

接下來我們使用圖文進行 React Diff 演演算法講解,希望可以更進一步瞭解 React 的 Diff 演演算法。

最簡單的 Diff 場景

上圖的 Diff 場景是最簡單的一種,新虛擬DOM 從左到右都能和老 Fiber 的節點一一匹配成功,協調位置的時候,老 Fiber A 的位置是 0,預設上一次協調返回的位置也是 0,根據協調位置規則,老 Fiber 的位置不比上一次協調返回的位置小,則只需要返回老 Fiber A 的位置 0 即可;

到了 B 進行協調位置的時候,老 Fiber B 位置 1 不比上一次協調返回的位置 0 小,則只需返回老 Fiber B 的位置 1 即可;到了 C 進行協調位置的時候,老 Fiber C 位置 2 不比上一次協調返回的位置 1 小,則只需要返回老 Fiber C 的位置 2 即可;

最後全部的新虛擬DOM 比對完畢,但老 Fiber 上還存在節點資訊,則需要將剩下的老 Fiber 進行刪除標記。

接下來我們看看複雜的 Diff 場景。

複雜的 Diff 場景

在上圖中,第一輪迴圈比對的時候,新虛擬節點A 和第一個老 Fiber 節點是可以匹配的,所以就可以複用老 Fiber 的節點資訊了,並且在協調的位置資訊的時候,是存在老 Fiber 的,那麼就去比較老 Fiber 的位置和上一次協調返回的位置進行比較(上一次協調返回的位置預設為 0),老 Fiber 的位置是等於新 Fiber 的位置,根據協調規則,位置不需要移動,返回老 Fiber 的位置資訊即可,很明顯這次返回的協調位置是 0。

到了第二個新虛擬節點 C 的時候,C 和老 Fiber 中的 B 是不匹配的,則第一輪比對結束。

第一輪比對結束之後,新虛擬DOM是還存在未比對的節點的,那麼繼續開始第二輪的比對。

在第二輪比對開始之前,會先將剩下未比對的老 Fiber 節點全部處理成一個 老 Fiber 的 key 或老 Fiber 的 index 為 key,Fiber 節點為 value 的 Map 中。

然後進行第二輪的比對。

虛擬DOM C 可以通過 C 的 key 值在老 Fiber 的 Map 中找到老 Fiber C 節點,這個時候會 C 進行暫存,然後把 Map 中的 C 進行刪除,再進行老 Fiber 的節點資訊複用,然後去協調比對位置資訊。

老 Fiber C 的位置是 2,然後上一次新 Fiber A 協調比對返回的位置資訊是 0,那麼這一次協調的位置是老 Fiber 的位置比上一次協調返回的位置大,那麼這次協調是不用標記 Placement 標記的,直接返回老 Fiber C 的位置 2。

虛擬DOM E,在老 Fiber 的 Map 中是沒有匹配成功的,所以在建立 Fiber E 的時候,是沒有進行老 Fiber 的複用的,去協調比對位置的時候,根據協調位置規則,沒有老 Fiber,就標記 Placement 並返回上一次協調返回的位置,那麼上一次 C 協調位置返回的位置資訊是 2,這一次 E 協調位置依然返回 2。

虛擬DOM B 也在 Fiber 的 Map 中匹配成功了,那麼匹配成功之後,就對老 Fiber B 進行暫存,然後刪除老 Fiber B,再進行資訊複用,然後又進行位置協調,老 Fiber B 的位置是 1,上一次協調返回的位置是 2,根據協調位置規則,老 Fiber 的位置小於上一次協調返回的位置,則標記 Placement 並返回上一次協調返回的位置 2。

最後,老 Fiber 的 Map 中還存在一個 D 節點沒處理,則需要對其進行刪除標記操作。

最終新 Fiber 將被協調成下面的樣子:

那麼根據圖片,我們又可以得出一個結論,匹配到的老 Fiber 如果和新 Fiber 相同或者在新 Fiber 位置的右邊則不需要進行移動標記。

Vue3 的 Diff 演演算法

在我看來 Vue3 的 Diff 演演算法是 Vue2、Vue3、React 的 Diff 演演算法中最複雜的一種。 下面我們來簡單說一下 Vue3 的 Diff 演演算法,只說陣列和陣列比對的情況。

第一輪,常見情況的比對

首先從左往右進行比對,如果是相同的就進行更新比對,如果不相同則停止比對,並且記錄停止的下標。 再從右往左進行比對,如果是相同的就進行更新比對,如果不相同也停止比對,也進行記錄停止的下標。 通過這樣左右進行比對,最後就可以把真正複雜部分進行範圍鎖定了。 左右比對完之後,如果新節點已經比對完了,老節點列表還存在節點未比對,則刪除老節點列表上的未比對的節點,如果老節點已經比對完了,新節點列表還存在未比對的節點則進行建立。

第二輪,複雜情況的比對

如果新節點未比對完,老節點也未比對完,則進行最後最複雜的處理。

先把剩下的新節點處理成節點的 key 為 key, 節點下標為 value 的 Map; 接著初始化一個長度為剩下未比對的新節點的長度的陣列 newIndexToOldIndexMap,初始化每個陣列的下標的預設值為 0。 再回圈剩下的舊節點,通過舊節點的 key 去剛剛建立的 Map 中查詢,看看舊節點有沒有在新節點中,如果舊節點沒有 key 則需要通過迴圈剩下的新節點進行查詢。 如果舊節點在新節點中沒找到,則說明該舊節點需要進行刪除。 如果找到了,則把找到的新節點的下標對應儲存到上述的陣列 newIndexToOldIndexMap 中,然後更新比對匹配到的新老節點。

把所有的舊節點比對完成後,就會得到一個剛剛收集的新節點的下標陣列,然後對這個新節點的下標陣列進行進行最長遞增子序列查詢得到一個最長遞增子序列的下標資料。 然後再進行迴圈左右對比完之後剩餘新節點的下標,然後判斷迴圈的下標是否被上述的陣列 newIndexToOldIndexMap 進行收集了,如果沒被收集到則說明這個新節點需要進行建立,如果已經被收集了則判斷該回圈的下標是否在上面計算得到的最長遞增子序列中,如果不在則需要對該回圈節點進行移動操作。

以上就是 Vue3 Diff 演演算法大概過程了。

更加詳細的 Vue3 Diff 演演算法解析可以檢視我這篇文章:vue3中的diff演演算法

Vue2 的 Diff 演演算法

Vue2 的 Diff 演演算法就是以新的虛擬DOM為準進行與老虛擬DOM的比對,繼而進行各種情況的處理。大概可以分為 4 種情況:更新節點、新增節點、刪除節點、移動節點位置。比對新老兩個虛擬DOM,就是通過迴圈,每回圈到一個新節點,就去老節點列表裡面找到和當前新節點相同的舊節點。如果在舊節點列表中找不到,說明當前節點是需要新增的節點,我們就需要進行建立節點並插入檢視的操作;如果找到了,就做更新操作;如果找到的舊節點與新節點位置不同,則需要移動節點等。

第一輪,簡單情況的比對

其中為了快速查詢到節點,Vue2 的 Diff 演演算法設定了 4 種優化策略,分別是:

  • 老陣列的開始與新陣列的開始
  • 老陣列的結尾與新陣列的結尾
  • 老陣列的開始與新陣列的結尾
  • 老陣列的結尾與新陣列的開始

通過這 4 種快捷的查詢方式,我們就不需要回圈來查詢了,只有當以上 4 種方式都查詢不到的時候,再進行迴圈查詢。

第二輪,不常見的情況的比對

最後迴圈結束後需要對未處理的節點進行處理。

如果是老節點列表先回圈完畢,這個時候如果新節點列表還有剩餘的節點,則說明這些節點都是需要新增的節點,直接把這些節點建立並插入到 DOM 中就行了。

如果是新節點列表先回圈完畢,這個時候如果老節點列表還有剩餘節點,則說明這些節點都是要被廢棄的節點,是應該被刪除的節點,直接批次刪除就可以了。

更加詳細的 Vue2 diff 演演算法可以檢視我這篇文章:Vue2 的 diff 演演算法詳解

React、Vue3、Vue2 的 Diff 演演算法對比

相同點

只有使用了虛擬DOM的這些框架,在進行更新 Diff 對比的時候,都是優先處理簡單的場景,再處理複雜的場景。

React 中是先處理左邊部分,左邊部分處理不了,再進行復雜部分的處理;Vue2 則先進行首尾、首首、尾尾部分的處理,然後再進行中間複雜部分的處理;Vue3 則先處理首尾部分,然後再處理中間複雜部分,Vue2 和 Vue3 最大的區別就是在處理中間複雜部分使用了最長遞增子序列演演算法找出穩定序列的部分。

在處理老節點部分,都需要把節點處理 key - value 的 Map 資料結構,方便在往後的比對中可以快速通過節點的 key 取到對應的節點。同樣在比對兩個新老節點是否相同時,key 是否相同也是非常重要的判斷標準。所以不同是 React, 還是 Vue,在寫動態列表的時候,都需要設定一個唯一值 key,這樣在 diff 演演算法處理的時候效能才最大化。

在移動或者建立節點的時候都使用了 insertBefore(newnode,existingnode) 這個 API:

  • newnode 必需。需要插入的節點物件。
  • existingnode 可選。在其之前插入新節點的子節點。如果未規定,則 insertBefore 方法會在結尾插入 newnode。

不同點

對靜態節點的處理不一樣。

由於 Vue 是通過 template 模版進行編譯的,所以在編譯的時候可以很好對靜態節點進行分析然後進行打修補程式標記,然後在 Diff 的時候,Vue2 是判斷如果是靜態節點則跳過過迴圈對比,而 Vue3 則是把整個靜態節點進行提升處理,Diff 的時候是不過進入迴圈的,所以 Vue3 比 Vue2 的 Diff 效能更高效。而 React 因為是通過 JSX 進行編譯的,是無法進行靜態節點分析的,所以 React 在對靜態節點處理這一塊是要遜色的。

Vue2 和 Vue3 的比對和更新是同步進行的,這個跟 React15 是相同的,就是在比對的過程中,如果發現了那些節點需要移動或者更新或刪除,是立即執行的,也就是 React 中常講的不可中斷的更新,如果比對量過大的話,就會造成卡頓,所以 React16 起就更改為了比對和更新是非同步進行的,所以 React16 以後的 Diff 是可以中斷,Diff 和任務排程都是在記憶體中進行的,所以即便中斷了,使用者也不會知道。

另外 Vue2 和 Vue3 都使用了雙端對比演演算法,而 React 的 Fiber 由於是單向連結串列的結構,所以在 React 不設定由右向左的連結串列之前,都無法實現雙端對比。那麼雙端對比目前 React 的 Diff 演演算法要好嗎?接下來我們來看看一個例子,看看它分別在 React、Vue2、Vue3 中的是怎麼處理的。

比如說我們現在有以下兩組新老節點:

老:A, B, C, D

新:D, A, B, C

那麼我們可以看到,新老兩組節點唯一的不同點就是,D 節點在新的節點中跑到開頭去了,像這種情況:

React 是從左向右進行比對的,在上述這種情況,React 需要把 A, B, C 三個節點分別移動到 D 節點的後面。

Vue2 在進行老節點的結尾與新節點的開始比對的時候,就發現這兩個節點是相同的,所以直接把老節點結尾的 D 移動到新節點開頭就行了,剩下的就只進行老節點的開始與新節點的開始進行比對,就可以發現它們的位置並沒有發生變化,不需要進行移動。

Vue3 是沒有了 Vue2 的新老首尾節點進行比較,只是從兩組節點的開頭和結尾進行比較,然後往中間靠攏,那麼 Vue3 在進行新老節點的開始和結尾比對的時候,都沒有比對成功,接下來就進行中間部分的比較,先把老節點處理成 key - value 的 Map 資料結構,然後又使用最長遞增子序列演演算法找出其中的穩定序列部分,也就是:A, B, C,然再對新節點進行迴圈比對,然後就會發現新節點的 A, B, C 都在穩定序列部分,不需要進行移動,然就只對 D,進行移動即可。

最後上述的例子在 Vue2 和 Vue3 中都只需要移動一個節點就可以完成 Diff 演演算法比對,而 React 在這種極端例子中則沒辦法進行很好的優化,需要進行多次節點移動操作。

為什麼 Vue 中不需要使用 Fiber

其實這個問題也可以叫做:為什麼 Vue 不需要時間分片?對於這個問題其實尤雨溪也在英文社群裡回答過,也有前端大牛翻譯釋出在公眾號上,那麼下面我也進行一下總結。

第一,首先時間分片是為了解決 CPU 進行大量計算的問題,因為 React 本身架構的問題,在預設的情況下更新會進行過多的計算,就算使用 React 提供的效能優化 API,進行設定,也會因為開發者本身的問題,依然可能存在過多計算的問題。

第二,而 Vue 通過響應式依賴跟蹤,在預設的情況下可以做到只進行元件樹級別的更新計算,而預設下 React 是做不到的(據說 React 已經在進行這方面的優化工作了),再者 Vue 是通過 template 進行編譯的,可以在編譯的時候進行非常好的效能優化,比如對靜態節點進行靜態節點提升的優化處理,而通過 JSX 進行編譯的 React 是做不到的。

第三,React 為了解決更新的時候進行過多計算的問題引入了時間分片,但同時又帶來了額外的計算開銷,就是任務協調的計算,雖然 React 也使用最小堆等的演演算法進行優化,但相對 Vue 還是多了額外的效能開銷,因為 Vue 沒有時間分片,所以沒有這方面的效能擔憂。

第四,根據研究表明,人類的肉眼對 100 毫秒以內的時間並不敏感,所以時間分片只對於處理超過 100 毫秒以上的計算才有很好的收益,而 Vue 的更新計算是很少出現 100 毫秒以上的計算的,所以 Vue 引入時間分片的收益並不划算。

總結

我們先由 “ React 的 Diff 演演算法為什麼不採用 Vue 的雙端對比的 Diff 演演算法?” 這個問題引出對 React 中的一些知識點的學習理解,比如什麼是 Fiber,Fiber 連結串列是如何生成的,然後詳細解析了 React Diff 演演算法,還對 React Diff 演演算法進行圖文並茂解析,讓我們可以更加理解 React 的 Diff 演演算法。 其後,我們又簡單介紹了 Vue3 和 Vue2 的 Diff 演演算法,之後對 React、Vue3、Vue2之間的演演算法的異同進行了講解。 最後我們又總結了一下尤雨溪對 “為什麼 Vue 不需要時間分片?” 這個問題的解析。

以上就是React Diff演演算法不採用Vue雙端對比演演算法原因詳解的詳細內容,更多關於React Diff演演算法不採用Vue雙端對比的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com