React的diff算法核心復用圖文詳解

引言

React 是基於 vdom 的前端框架,組件 render 產生 vdom,然後渲染器把 vdom 渲染出來。

state 更新的時候,組件會重新 render,產生新的 vdom,在瀏覽器平臺下,為瞭減少 dom 的創建,React 會對兩次的 render 結果做 diff,盡量復用 dom,提高性能。

diff 算法是前端框架中比較復雜的部分,代碼比較多,但今天我們不上代碼,隻看圖來理解它。

首先,我們先過一下 react 的 fiber 架構:

Fiber 架構

React 是通過 jsx 描述頁面結構的:

const profile = {
    return <div>
        <img src="avatar.png" className="profile" />
        <h3>{[user.firstName, user.lastName].join(" ")}</h3>
    </div>

經過 babel 等的編譯會變成 render function:

import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const profile = _jsxs("div", {
  children: [
    _jsx("img", {
      src: "avatar.png",
      className: "profile",
    }),
    _jsx("h3", {
      children: [user.firstName, user.lastName].join(" "),
    }),
  ],
});

render function 執行結果就是 vdom,也就是 React Element 的實例:

在 16 之前,React 是直接遞歸渲染 vdom 的,setState 會觸發重新渲染,對比渲染出的新舊 vdom,對差異部分進行 dom 操作。

在 16 之後,為瞭優化性能,會先把 vdom 轉換成 fiber,也就是從樹轉換成鏈表,然後再渲染。整體渲染流程分成瞭兩個階段:

  • render 階段:從 vdom 轉換成 fiber,並且對需要 dom 操作的節點打上 effectTag 的標記
  • commit 階段:對有 effectTag 標記的 fiber 節點進行 dom 操作,並執行所有的 effect 副作用函數。

從 vdom 轉成 fiber 的過程叫做 reconcile(調和),這個過程是可以打斷的,由 scheduler 調度執行。

diff 算法作用在 reconcile 階段:

第一次渲染不需要 diff,直接 vdom 轉 fiber。

再次渲染的時候,會產生新的 vdom,這時候要和之前的 fiber 做下對比,決定怎麼產生新的 fiber,對可復用的節點打上修改的標記,剩餘的舊節點打上刪除標記,新節點打上新增標記。

接下來我們就來詳細瞭解下 React 的 diff 算法:

React 的 diff 算法

在講 diff 算法實現之前,我們要先想明白為什麼要做 diff,不做行麼?

當然可以,每一次渲染都直接把 vdom 轉成 fiber 就行,不用和之前的做對比,這樣是可行的。

其實 SSR 的時候就不用做 diff,因為會把組件渲染成字符串,第二次渲染也是產生字符串,難道這時候還要和之前的字符串對比下,有哪些字符串可以復用麼?

不需要,SSR 的時候就沒有 diff,每次都是 vdom 渲染出新的字符串。

那為什麼瀏覽器裡要做 diff 呢?

因為 dom 創建的性能成本很高,如果不做 dom 的復用,那前端框架的性能就太差瞭。

diff 算法的目的就是對比兩次渲染結果,找到可復用的部分,然後剩下的該刪除刪除,該新增新增。

那具體怎麼實現 React 的 diff 算法呢?

比如父節點下有 A、B、C、D 四個子節點,那渲染出的 vdom 就是這樣的:

經過 reconcile 之後,會變成這樣的 fiber 結構:

那如果再次渲染的時候,渲染出瞭 A、C、B、E 的 vdom,這時候怎麼處理呢?

再次渲染出 vdom 的時候,也要進行 vdom 轉 fiber 的 reconcile 階段,但是要盡量能復用之前的節點。

那怎麼復用呢?

一一對比下不就行瞭?

先把之前的 fiber 節點放到一個 map 裡,key 就是節點的 key:

然後每個新的 vdom 都去這個 map 裡查找下有沒有可以復用的,找到瞭的話就移動過來,打上更新的 effectTag:

這樣遍歷完 vdom 節點之後,map 裡剩下一些,這些是不可復用的,那就刪掉,打上刪除的 effectTag;如果 vdom 中還有一些沒找到復用節點的,就直接創建,打上新增的 effectTag。

這樣就實現瞭更新時的 reconcile,也就是上面的 diff 算法。其實核心就是找到可復用的節點,剩下的舊節點刪掉,新節點新增。

但有的時候可以再簡化一下,比如上次渲染是 A、B、C、D,這次渲染也是 A、B、C、D,那直接順序對比下就行,沒必要建立 map 再找。

所以 React 的 diff 算法是分成兩次遍歷的:

第一輪遍歷,一一對比 vdom 和老的 fiber,如果可以復用就處理下一個節點,否則就結束遍歷。

如果所有的新的 vdom 處理完瞭,那就把剩下的老 fiber 節點刪掉就行。

如果還有 vdom 沒處理,那就進行第二次遍歷:

第二輪遍歷,把剩下的老 fiber 放到 map 裡,遍歷剩下的 vdom,從 map 裡查找,如果找到瞭,就移動過來。

第二輪遍歷完瞭之後,把剩餘的老 fiber 刪掉,剩餘的 vdom 新增。

這樣就完成瞭新的 fiber 結構的創建,也就是 reconcile 的過程。

比如上面那個例子,第一輪遍歷就是這樣的:

一一對比新的 vdom 和 老的 fiber,發現 A 是可以復用的,那就創建新 fiber 節點,打上更新標記。

C 不可復用,所以結束第一輪遍歷,進入第二輪遍歷。

把剩下的 老 fiber 節點放到 map 裡,然後遍歷新的 vdom 節點,從 map 中能找到的話,就是可復用,移動過來打上更新的標記。

遍歷完之後,剩下的老 fiber 節點刪掉,剩下的新 vdom 新增。

這樣就完成瞭更新時的 reconcile 的過程。

總結

react 是基於 vdom 的前端框架,組件渲染產生 vdom,渲染器把 vdom 渲染成 dom。

瀏覽器下使用 react-dom 的渲染器,會先把 vdom 轉成 fiber,找到需要更新 dom 的部分,打上增刪改的 effectTag 標記,這個過程叫做 reconcile,可以打斷,由 scheducler 調度執行。reconcile 結束之後一次性根據 effectTag 更新 dom,叫做 commit。

這就是 react 的基於 fiber 的渲染流程,分成 render(reconcile + schedule)、commit 兩個階段。

當渲染完一次,產生瞭 fiber 之後,再次渲染的 vdom 要和之前的 fiber 對比下,再決定如何產生新的 fiber,目標是盡可能復用已有的 fiber 節點,這叫做 diff 算法。

react 的 diff 算法分為兩個階段:

第一個階段一一對比,如果可以復用就下一個,不可以復用就結束。

第二個階段把剩下的老 fiber 放到 map 裡,遍歷剩餘的 vdom,一一查找 map 中是否有可復用的節點。

最後把剩下的老 fiber 刪掉,剩下的新 vdom 新增。

這樣就完成瞭更新時的 reconcile 過程。

其實 diff 算法的核心就是復用節點,通過一一對比也好,通過 map 查找也好,都是為瞭找到可復用的節點,移動過來。然後剩下的該刪刪該增增。

理解瞭如何找到可復用的節點,就理解瞭 diff 算法的核心。

以上就是 React的diff算法核心復用詳解的詳細內容,更多關於React diff 算法復用的資料請關註WalkonNet其它相關文章!

推薦閱讀: