欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2022年07月07日 08:48:13   作者:Cobyte  
這篇文章主要介紹了React?Diff算法不采用Vue雙端對比算法原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

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

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

最后講一下為什么 Vue 中不需要使用 Fiber 架構(gòu)。

React 官方的解析

其實為什么 React 不采用 Vue 的雙端對比算法,React 官方已經(jīng)在源碼的注釋里已經(jīng)說明了,我們來看一下 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 算法優(yōu)化是因為目前 Fiber 上沒有設置反向鏈表,而且想知道就目前這種方案能持續(xù)多久,如果目前這種模式不理想的話,那么也可以增加雙端對比算法。

即使是雙端對比算法,我們也要對這種情況進行優(yōu)化,我們應該使用 Map 這種數(shù)據(jù)結(jié)構(gòu)方案去替代原來那種幾乎沒有什么變化也進行暴力比較的方案。它第一次搜索循環(huán)是通過 forward-only 這種模式(就是只從左向右查找),(第一次循環(huán)可能還沒有結(jié)束,還有節(jié)點沒有比對的時候)如果還要繼續(xù)向前循環(huán)查找那么就要通過 Map 這種數(shù)據(jù)類型了。(就目前這個單向鏈表的數(shù)據(jù)結(jié)構(gòu),如果采用)雙端對比查找算法比較難控制它反向查找的,但它確實是一種成功的算法。此外,雙端對比算法的實現(xiàn)也在我們的工作迭代當中。

第一次迭代,我們就先將就使用這種不好的方案吧,每次新增/移動都要添加所有的數(shù)據(jù)到一個 Map 的數(shù)據(jù)類型對象中。

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

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

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

Fiber 的結(jié)構(gòu)

在 React15 以前 React 的組件更新創(chuàng)建虛擬 DOM 和 Diff 的過程是不可中斷,如果需要更新組件樹層級非常深的話,在 Diff 的過程會非常占用瀏覽器的線程,而我們都知道瀏覽器執(zhí)行JavaScript 的線程和渲染真實 DOM 的線程是互斥的,也就是同一時間內(nèi),瀏覽器要么在執(zhí)行 JavaScript 的代碼運算,要么在渲染頁面,如果 JavaScript 的代碼運行時間過長則會造成頁面卡頓。 基于以上原因 React 團隊在 React16 之后就改寫了整個架構(gòu),將原來數(shù)組結(jié)構(gòu)的虛擬DOM,改成叫 Fiber 的一種數(shù)據(jù)結(jié)構(gòu),基于這種 Fiber 的數(shù)據(jù)結(jié)構(gòu)可以實現(xiàn)由原來不可中斷的更新過程變成異步的可中斷的更新。

Fiber 的數(shù)據(jù)結(jié)構(gòu)主要長成以下的樣子,主要通過 Fiber 的一些屬性去保存組件相關的信息。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作為靜態(tài)數(shù)據(jù)結(jié)構(gòu)的屬性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于連接其他Fiber節(jié)點形成Fiber樹
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作為動態(tài)的工作單元的屬性
  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;

  // 調(diào)度優(yōu)先級相關
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

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

Fiber 主要靠以下屬性連成一棵樹結(jié)構(gòu)的數(shù)據(jù)的,也就是 Fiber 鏈表。

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

舉個例子,如下的組件結(jié)構(gòu):

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

對應的 Fiber 鏈表結(jié)構(gòu):

那么以上的 Fiber 鏈表的數(shù)據(jù)結(jié)構(gòu)有什么特點,就是任何一個位置的 Fiber 節(jié)點,都可以非常容易知道它的父 Fiber, 第一個子元素的 Fiber,和它的兄弟節(jié)點 Fiber。卻不容易知道它前一個 Fiber 節(jié)點是誰,這就是 React 中單向鏈表 Fiber 節(jié)點的特點。也正是因為這些即便在協(xié)調(diào)的過程被中斷了,再恢復協(xié)調(diào)的時候,依然知道當前的 父節(jié)點和孩子節(jié)點等信息。

那么 React 是將對應組件怎么生成一個 Fiber 鏈表數(shù)據(jù)的呢?

Fiber 鏈表的生成

上面的組件在經(jīng)過 JSX 的編譯之后,初始化的時候會生成成一個類似于 React 15 或者 Vue 那種虛擬 DOM 的數(shù)據(jù)結(jié)構(gòu)。然后創(chuàng)建一個叫 fiberRoot 的 Fiber 節(jié)點,然后開始從 fiberRoot 這個根 Fiber 開始進行協(xié)調(diào),生成一棵 Fiber 樹,這個棵樹被稱為:workInProgress Fiber 樹 ,意思是正在工作的 Fiber 樹,接下來我們詳細了解一下具體是怎么生成一棵 Fiber 樹的。要先了解 Fiber 樹的生成原理才更好去理解 Fiber 樹 diff 的過程。

以下是一段簡單版的 Fiber 鏈表生成的代碼片段。 這個協(xié)調(diào)子節(jié)點的函數(shù)接收兩個參數(shù),returnFiber 是父 Fiber,children 是這個節(jié)點的子元素的虛擬 DOM數(shù)據(jù)。

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

構(gòu)建完的 workInProgress Fiber樹 會在 commit階段 渲染到頁面。

在組件狀態(tài)數(shù)據(jù)發(fā)生變更的時候,會根據(jù)最新的狀態(tài)數(shù)據(jù)先會生成新的虛擬DOM,再去構(gòu)建一棵新的 workInProgress Fiber 樹 ,而在重新協(xié)調(diào)構(gòu)建新的 Fiber 樹的過程也就是 React Diff 發(fā)生的地方。接下來,我們就看看 React Diff 算法是怎么樣的。

React 的 Diff 算法

深度優(yōu)先,有子節(jié)點,就遍歷子節(jié)點,沒有子節(jié)點,就找兄弟節(jié)點,沒有兄弟節(jié)點,就找叔叔節(jié)點,叔叔節(jié)點也沒有的話,就繼續(xù)往上找,它爺爺?shù)男值埽绻恢睕]找到,就代表所有的更新任務都更新完畢了。

重點是在更新自己的同時需要去協(xié)調(diào)子節(jié)點,也就是傳說中進行 Diff 的地方。

進入?yún)f(xié)調(diào)的時候它自己就是父 Fiber,它的子節(jié)點在協(xié)調(diào)之前,是剛剛通過更新的狀態(tài)數(shù)據(jù)生成的最新的虛擬DOM數(shù)據(jù),是個數(shù)組結(jié)構(gòu)的元素數(shù)據(jù)。

那么要進行更新,就肯定是以為最新的節(jié)點數(shù)據(jù)為準了,又因為最新的節(jié)點數(shù)據(jù)是一個數(shù)組,所以可以進行循環(huán)對比每一個節(jié)點,很明顯這個循環(huán)是從左向右進行查找比對的。

第一輪,常見情況的比對

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

怎么比對呢?

在循環(huán)的新節(jié)點虛擬DOM數(shù)據(jù)的時候,拿到新節(jié)點虛擬DOM信息,然后就去和老 Fiber 節(jié)點進行比對,如果兩個節(jié)點相同則創(chuàng)建一個新的 Fiber 節(jié)點并復用一些老 Fiber 節(jié)點的信息,比如真實 DOM,并給這個新的 Fiber 節(jié)點打上一個 Update 的標記,代表這個節(jié)點需要更新即可。

接著去更新協(xié)調(diào)位置信息。

在循環(huán)的最后進行 Fiber 鏈表的處理:

如果是頭節(jié)點,則把新 Fiber 設置為父 Fiber 的 child 屬性的值; 如果不是頭節(jié)點,則把新 Fiber 設置為上一輪循環(huán)的創(chuàng)建的 Fiber 節(jié)點的 sibing 屬性的值; 更新上一輪 Fiber 變量的值,就是把這一輪的 Fiber 設置成下一輪的 Fiber; 更新比對的老 Fiber 的值。

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

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

如果經(jīng)過第一輪比對,新節(jié)點還存在未比對的,則繼續(xù)循環(huán)查找。

先將剩下未比對的老 Fiber 節(jié)點全部處理成一個 老 Fiber 的 key 或老 Fiber 的 index 為 key,F(xiàn)iber 節(jié)點為 value 的 Map 中,這樣就可以,以 O(1) 復雜度,通過新 Fiber 的 key 去 Map 對象中查找匹配的 Fiber,找到了,則刪除 Map 對象中的老 Fiber 數(shù)據(jù),然后復用匹配到的 Fiber 數(shù)據(jù)。

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

在循環(huán)的最后進行 Fiber 鏈表的處理:

如果是頭節(jié)點,則把新 Fiber 設置為父 Fiber 的 child 屬性的值; 如果不是頭節(jié)點,則把新 Fiber 設置為上一輪循環(huán)的創(chuàng)建的 Fiber 節(jié)點的 sibing 屬性的值; 更新上一輪 Fiber 變量的值,就是把這一輪的 Fiber 設置成下一輪的 Fiber; 更新比對的老 Fiber 的值。

重點如何協(xié)調(diào)更新位置信息

如果是初始渲染,那么協(xié)調(diào)位置就只是記錄當前元素下標的位置到 Fiber 節(jié)點上。如果是更新階段,就先判斷有沒有老 Fiber 節(jié)點,如果沒有老 Fiber 節(jié)點,則說明該節(jié)點需要創(chuàng)建,就給當前新的 Fiber 節(jié)點打上一個 Placement 的標記,如果有老 Fiber 節(jié)點,則判斷老 Fiber 節(jié)點的位置是否比上一次協(xié)調(diào)的返回的位置小,如果是,則說明該節(jié)點需要移動,給新 Fiber 節(jié)點打上一個 Placement 的標記,并繼續(xù)返回上一次協(xié)調(diào)返回的位置;如果老 Fiber 節(jié)點的位置大或者等于上一次協(xié)調(diào)返回的位置,則說明該節(jié)點不需要進行位置移動操作,就返回老 Fiber 的位置即可。

這里需要說明的一點,為什么移動和新增節(jié)點都是 Placement 的標記呢?

因為我們是在協(xié)調(diào)一個子節(jié)點列表,所以不管是新增還是移動都是屬于位置是需要發(fā)生變化的,所以新增和移動都是同一種操作情況。

小結(jié)

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

  • 第一輪,從左向右新老節(jié)點進行比對查找能復用的舊節(jié)點,如果有新老節(jié)點比對不成功的,則停止這一輪的比對,并記錄了停止的位置。
  • 如果第一輪比對,能把所有的新節(jié)點都比對完畢,則刪除舊節(jié)點還沒進行比對的節(jié)點。
  • 如果第一輪的比對,沒能將所有的新節(jié)點都比對完畢,則繼續(xù)從第一輪比對停止的位置繼續(xù)開始循環(huán)新節(jié)點,拿每一個新節(jié)點去老節(jié)點里面進行查找,有匹配成功的則復用,沒匹配成功的則在協(xié)調(diào)位置的時候打上 Placement 的標記。
  • 在所有新節(jié)點比對完畢之后,檢查還有沒有沒進行復用的舊節(jié)點,如果有,則全部刪除。

圖文解釋 React Diff 算法

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

最簡單的 Diff 場景

上圖的 Diff 場景是最簡單的一種,新虛擬DOM 從左到右都能和老 Fiber 的節(jié)點一一匹配成功,協(xié)調(diào)位置的時候,老 Fiber A 的位置是 0,默認上一次協(xié)調(diào)返回的位置也是 0,根據(jù)協(xié)調(diào)位置規(guī)則,老 Fiber 的位置不比上一次協(xié)調(diào)返回的位置小,則只需要返回老 Fiber A 的位置 0 即可;

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

最后全部的新虛擬DOM 比對完畢,但老 Fiber 上還存在節(jié)點信息,則需要將剩下的老 Fiber 進行刪除標記。

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

復雜的 Diff 場景

在上圖中,第一輪循環(huán)比對的時候,新虛擬節(jié)點A 和第一個老 Fiber 節(jié)點是可以匹配的,所以就可以復用老 Fiber 的節(jié)點信息了,并且在協(xié)調(diào)的位置信息的時候,是存在老 Fiber 的,那么就去比較老 Fiber 的位置和上一次協(xié)調(diào)返回的位置進行比較(上一次協(xié)調(diào)返回的位置默認為 0),老 Fiber 的位置是等于新 Fiber 的位置,根據(jù)協(xié)調(diào)規(guī)則,位置不需要移動,返回老 Fiber 的位置信息即可,很明顯這次返回的協(xié)調(diào)位置是 0。

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

第一輪比對結(jié)束之后,新虛擬DOM是還存在未比對的節(jié)點的,那么繼續(xù)開始第二輪的比對。

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

然后進行第二輪的比對。

虛擬DOM C 可以通過 C 的 key 值在老 Fiber 的 Map 中找到老 Fiber C 節(jié)點,這個時候會 C 進行暫存,然后把 Map 中的 C 進行刪除,再進行老 Fiber 的節(jié)點信息復用,然后去協(xié)調(diào)比對位置信息。

老 Fiber C 的位置是 2,然后上一次新 Fiber A 協(xié)調(diào)比對返回的位置信息是 0,那么這一次協(xié)調(diào)的位置是老 Fiber 的位置比上一次協(xié)調(diào)返回的位置大,那么這次協(xié)調(diào)是不用標記 Placement 標記的,直接返回老 Fiber C 的位置 2。

虛擬DOM E,在老 Fiber 的 Map 中是沒有匹配成功的,所以在創(chuàng)建 Fiber E 的時候,是沒有進行老 Fiber 的復用的,去協(xié)調(diào)比對位置的時候,根據(jù)協(xié)調(diào)位置規(guī)則,沒有老 Fiber,就標記 Placement 并返回上一次協(xié)調(diào)返回的位置,那么上一次 C 協(xié)調(diào)位置返回的位置信息是 2,這一次 E 協(xié)調(diào)位置依然返回 2。

虛擬DOM B 也在 Fiber 的 Map 中匹配成功了,那么匹配成功之后,就對老 Fiber B 進行暫存,然后刪除老 Fiber B,再進行信息復用,然后又進行位置協(xié)調(diào),老 Fiber B 的位置是 1,上一次協(xié)調(diào)返回的位置是 2,根據(jù)協(xié)調(diào)位置規(guī)則,老 Fiber 的位置小于上一次協(xié)調(diào)返回的位置,則標記 Placement 并返回上一次協(xié)調(diào)返回的位置 2。

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

最終新 Fiber 將被協(xié)調(diào)成下面的樣子:

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

Vue3 的 Diff 算法

在我看來 Vue3 的 Diff 算法是 Vue2、Vue3、React 的 Diff 算法中最復雜的一種。 下面我們來簡單說一下 Vue3 的 Diff 算法,只說數(shù)組和數(shù)組比對的情況。

第一輪,常見情況的比對

首先從左往右進行比對,如果是相同的就進行更新比對,如果不相同則停止比對,并且記錄停止的下標。 再從右往左進行比對,如果是相同的就進行更新比對,如果不相同也停止比對,也進行記錄停止的下標。 通過這樣左右進行比對,最后就可以把真正復雜部分進行范圍鎖定了。 左右比對完之后,如果新節(jié)點已經(jīng)比對完了,老節(jié)點列表還存在節(jié)點未比對,則刪除老節(jié)點列表上的未比對的節(jié)點,如果老節(jié)點已經(jīng)比對完了,新節(jié)點列表還存在未比對的節(jié)點則進行創(chuàng)建。

第二輪,復雜情況的比對

如果新節(jié)點未比對完,老節(jié)點也未比對完,則進行最后最復雜的處理。

先把剩下的新節(jié)點處理成節(jié)點的 key 為 key, 節(jié)點下標為 value 的 Map; 接著初始化一個長度為剩下未比對的新節(jié)點的長度的數(shù)組 newIndexToOldIndexMap,初始化每個數(shù)組的下標的默認值為 0。 再循環(huán)剩下的舊節(jié)點,通過舊節(jié)點的 key 去剛剛創(chuàng)建的 Map 中查找,看看舊節(jié)點有沒有在新節(jié)點中,如果舊節(jié)點沒有 key 則需要通過循環(huán)剩下的新節(jié)點進行查找。 如果舊節(jié)點在新節(jié)點中沒找到,則說明該舊節(jié)點需要進行刪除。 如果找到了,則把找到的新節(jié)點的下標對應存儲到上述的數(shù)組 newIndexToOldIndexMap 中,然后更新比對匹配到的新老節(jié)點。

把所有的舊節(jié)點比對完成后,就會得到一個剛剛收集的新節(jié)點的下標數(shù)組,然后對這個新節(jié)點的下標數(shù)組進行進行最長遞增子序列查找得到一個最長遞增子序列的下標數(shù)據(jù)。 然后再進行循環(huán)左右對比完之后剩余新節(jié)點的下標,然后判斷循環(huán)的下標是否被上述的數(shù)組 newIndexToOldIndexMap 進行收集了,如果沒被收集到則說明這個新節(jié)點需要進行創(chuàng)建,如果已經(jīng)被收集了則判斷該循環(huán)的下標是否在上面計算得到的最長遞增子序列中,如果不在則需要對該循環(huán)節(jié)點進行移動操作。

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

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

Vue2 的 Diff 算法

Vue2 的 Diff 算法就是以新的虛擬DOM為準進行與老虛擬DOM的比對,繼而進行各種情況的處理。大概可以分為 4 種情況:更新節(jié)點、新增節(jié)點、刪除節(jié)點、移動節(jié)點位置。比對新老兩個虛擬DOM,就是通過循環(huán),每循環(huán)到一個新節(jié)點,就去老節(jié)點列表里面找到和當前新節(jié)點相同的舊節(jié)點。如果在舊節(jié)點列表中找不到,說明當前節(jié)點是需要新增的節(jié)點,我們就需要進行創(chuàng)建節(jié)點并插入視圖的操作;如果找到了,就做更新操作;如果找到的舊節(jié)點與新節(jié)點位置不同,則需要移動節(jié)點等。

第一輪,簡單情況的比對

其中為了快速查找到節(jié)點,Vue2 的 Diff 算法設置了 4 種優(yōu)化策略,分別是:

  • 老數(shù)組的開始與新數(shù)組的開始
  • 老數(shù)組的結(jié)尾與新數(shù)組的結(jié)尾
  • 老數(shù)組的開始與新數(shù)組的結(jié)尾
  • 老數(shù)組的結(jié)尾與新數(shù)組的開始

通過這 4 種快捷的查找方式,我們就不需要循環(huán)來查找了,只有當以上 4 種方式都查找不到的時候,再進行循環(huán)查找。

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

最后循環(huán)結(jié)束后需要對未處理的節(jié)點進行處理。

如果是老節(jié)點列表先循環(huán)完畢,這個時候如果新節(jié)點列表還有剩余的節(jié)點,則說明這些節(jié)點都是需要新增的節(jié)點,直接把這些節(jié)點創(chuàng)建并插入到 DOM 中就行了。

如果是新節(jié)點列表先循環(huán)完畢,這個時候如果老節(jié)點列表還有剩余節(jié)點,則說明這些節(jié)點都是要被廢棄的節(jié)點,是應該被刪除的節(jié)點,直接批量刪除就可以了。

更加詳細的 Vue2 diff 算法可以查看我這篇文章:Vue2 的 diff 算法詳解

React、Vue3、Vue2 的 Diff 算法對比

相同點

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

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

在處理老節(jié)點部分,都需要把節(jié)點處理 key - value 的 Map 數(shù)據(jù)結(jié)構(gòu),方便在往后的比對中可以快速通過節(jié)點的 key 取到對應的節(jié)點。同樣在比對兩個新老節(jié)點是否相同時,key 是否相同也是非常重要的判斷標準。所以不同是 React, 還是 Vue,在寫動態(tài)列表的時候,都需要設置一個唯一值 key,這樣在 diff 算法處理的時候性能才最大化。

在移動或者創(chuàng)建節(jié)點的時候都使用了 insertBefore(newnode,existingnode) 這個 API:

  • newnode 必需。需要插入的節(jié)點對象。
  • existingnode 可選。在其之前插入新節(jié)點的子節(jié)點。如果未規(guī)定,則 insertBefore 方法會在結(jié)尾插入 newnode。

不同點

對靜態(tài)節(jié)點的處理不一樣。

由于 Vue 是通過 template 模版進行編譯的,所以在編譯的時候可以很好對靜態(tài)節(jié)點進行分析然后進行打補丁標記,然后在 Diff 的時候,Vue2 是判斷如果是靜態(tài)節(jié)點則跳過過循環(huán)對比,而 Vue3 則是把整個靜態(tài)節(jié)點進行提升處理,Diff 的時候是不過進入循環(huán)的,所以 Vue3 比 Vue2 的 Diff 性能更高效。而 React 因為是通過 JSX 進行編譯的,是無法進行靜態(tài)節(jié)點分析的,所以 React 在對靜態(tài)節(jié)點處理這一塊是要遜色的。

Vue2 和 Vue3 的比對和更新是同步進行的,這個跟 React15 是相同的,就是在比對的過程中,如果發(fā)現(xiàn)了那些節(jié)點需要移動或者更新或刪除,是立即執(zhí)行的,也就是 React 中常講的不可中斷的更新,如果比對量過大的話,就會造成卡頓,所以 React16 起就更改為了比對和更新是異步進行的,所以 React16 以后的 Diff 是可以中斷,Diff 和任務調(diào)度都是在內(nèi)存中進行的,所以即便中斷了,用戶也不會知道。

另外 Vue2 和 Vue3 都使用了雙端對比算法,而 React 的 Fiber 由于是單向鏈表的結(jié)構(gòu),所以在 React 不設置由右向左的鏈表之前,都無法實現(xiàn)雙端對比。那么雙端對比目前 React 的 Diff 算法要好嗎?接下來我們來看看一個例子,看看它分別在 React、Vue2、Vue3 中的是怎么處理的。

比如說我們現(xiàn)在有以下兩組新老節(jié)點:

老:A, B, C, D

新:D, A, B, C

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

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

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

Vue3 是沒有了 Vue2 的新老首尾節(jié)點進行比較,只是從兩組節(jié)點的開頭和結(jié)尾進行比較,然后往中間靠攏,那么 Vue3 在進行新老節(jié)點的開始和結(jié)尾比對的時候,都沒有比對成功,接下來就進行中間部分的比較,先把老節(jié)點處理成 key - value 的 Map 數(shù)據(jù)結(jié)構(gòu),然后又使用最長遞增子序列算法找出其中的穩(wěn)定序列部分,也就是:A, B, C,然再對新節(jié)點進行循環(huán)比對,然后就會發(fā)現(xiàn)新節(jié)點的 A, B, C 都在穩(wěn)定序列部分,不需要進行移動,然就只對 D,進行移動即可。

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

為什么 Vue 中不需要使用 Fiber

其實這個問題也可以叫做:為什么 Vue 不需要時間分片?對于這個問題其實尤雨溪也在英文社區(qū)里回答過,也有前端大牛翻譯發(fā)布在公眾號上,那么下面我也進行一下總結(jié)。

第一,首先時間分片是為了解決 CPU 進行大量計算的問題,因為 React 本身架構(gòu)的問題,在默認的情況下更新會進行過多的計算,就算使用 React 提供的性能優(yōu)化 API,進行設置,也會因為開發(fā)者本身的問題,依然可能存在過多計算的問題。

第二,而 Vue 通過響應式依賴跟蹤,在默認的情況下可以做到只進行組件樹級別的更新計算,而默認下 React 是做不到的(據(jù)說 React 已經(jīng)在進行這方面的優(yōu)化工作了),再者 Vue 是通過 template 進行編譯的,可以在編譯的時候進行非常好的性能優(yōu)化,比如對靜態(tài)節(jié)點進行靜態(tài)節(jié)點提升的優(yōu)化處理,而通過 JSX 進行編譯的 React 是做不到的。

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

第四,根據(jù)研究表明,人類的肉眼對 100 毫秒以內(nèi)的時間并不敏感,所以時間分片只對于處理超過 100 毫秒以上的計算才有很好的收益,而 Vue 的更新計算是很少出現(xiàn) 100 毫秒以上的計算的,所以 Vue 引入時間分片的收益并不劃算。

總結(jié)

我們先由 “ React 的 Diff 算法為什么不采用 Vue 的雙端對比的 Diff 算法?” 這個問題引出對 React 中的一些知識點的學習理解,比如什么是 Fiber,F(xiàn)iber 鏈表是如何生成的,然后詳細解析了 React Diff 算法,還對 React Diff 算法進行圖文并茂解析,讓我們可以更加理解 React 的 Diff 算法。 其后,我們又簡單介紹了 Vue3 和 Vue2 的 Diff 算法,之后對 React、Vue3、Vue2之間的算法的異同進行了講解。 最后我們又總結(jié)了一下尤雨溪對 “為什么 Vue 不需要時間分片?” 這個問題的解析。

以上就是React Diff算法不采用Vue雙端對比算法原因詳解的詳細內(nèi)容,更多關于React Diff算法不采用Vue雙端對比的資料請關注腳本之家其它相關文章!

相關文章

最新評論