圖解 React diff 算法

Render 階段會生成 Fiber Tree,所謂的 diff 實際上就是發生在這個階段,這里的 diff 指的是 current FiberNode 和 JSX 對象之間進行對比,然后生成新的的 wip FiberNode。

除了 React 以外,其他使用到了虛擬 DOM 的前端框架也會有類似的流程,比如 Vue 里面將這個流程稱之為 patch。

diff 算法本身是有性能上面的消耗,在 React 文檔中有提到,即便采用最前沿的算法,如果要完整的對比兩棵樹,那么算法的復雜度都會達到 O(n^3),n 代表的是元素的數量,如果 n 為 1000,要執行的計算量會達到十億量級的級別。

因此,為了降低算法的復雜度,React 為 diff 算法設置了 3 個限制:

  • 限制一:只對同級別元素進行 diff,如果一個 DOM 元素在前后兩次更新中跨越了層級,那么 React 不會嘗試復用它
  • 限制二:兩個不同類型的元素會產生不同的樹。比如元素從 div 變成了 p,那么 React 會直接銷毀 div 以及子孫元素,新建 p 以及 p 對應的子孫元素
  • 限制三:開發者可以通過 key 來暗示哪些子元素能夠保持穩定

更新前:

<div><p key="one">one</p><h3 key="two">two</h3>
</div>

更新后

<div><h3 key="two">two</h3><p key="one">one</p>
</div>

如果沒有 key,那么 React 就會認為 div 的第一個子元素從 p 變成了 h3,第二個子元素從 h3 變成了 p,因此 React 就會采用限制二的規則。

但是如果使用了 key,那么此時的 DOM 元素是可以復用的,只不過前后交換了位置而已。

接下來我們回頭再來看限制一,對同級元素進行 diff,究竟是如何進行 diff ?整個 diff 的流程可以分為兩大類:

  • 更新后只有一個元素,此時就會根據 newChild 創建對應的 wip FiberNode,對應的流程就是單節點 diff
  • 更新后有多個元素,此時就會遍歷 newChild 創建對應的 wip FiberNode 以及它的兄弟元素,此時對應的流程就是多節點 diff

單節點 diff

單節點指的是新節點為單一節點,但是舊節點的數量是不一定

單節點 diff 是否能夠復用遵循以下的流程:

  • 步驟一 :判斷 key 是否相同
  • 如果更新前后沒有設置 key,那么 key 就是 null,也是屬于相同的情況
  • 如果 key 相同,那么就會進入到步驟二
  • 如果 key 不同,就不需要進入步驟二,無需判斷 type,結果直接為不能復用(如果有兄弟節點還會去遍歷兄弟節點)
  • 步驟二 :如果 key 相同,再判斷 type 是否相同
  • 如果 type 相同,那么就復用
  • 如果 type 不同,無法復用(并且兄弟節點也一并標記為刪除)

更新前

<ul><li>1</li><li>2</li><li>3</li>
</ul>

更新后

<ul><p>1</p>
</ul>

這里因為沒有設置 key,所以會被設為 key 是相同的,接下來就會進入到 type 的判斷,此時發現 type 不同,因此不能夠復用。既然這里唯一的可能性都已經不能夠復用,會直接標記兄弟 FiberNode 為刪除狀態。

如果上面的例子中,key 不同只能代表當前的 FiberNode 無法復用,因此還需要去遍歷兄弟的 FiberNode

下面我們再來看一些示例

更新前

<div>one</div>

更新后

<p>one</p>

沒有設置 key,那么可以認為默認 key 就是 null,更新前后兩個 key 是相同的,接下來就查看 type,發現 type 不同,因此不能復用。

更新前

<div key="one">one</div>

更新后

<div key="two">one</div>

更新前后 key 不同,不需要再判斷 type,結果為不能復用

更新前

<div key="one">one</div>

更新后

<p key="two">one</p>

更新前后 key 不同,不需要再判斷 type,結果為不能復用

更新前

<div key="one">one</div>

更新后

<div key="one">two</div>

首先判斷 key 相同,接下來判斷 type 發現也是相同,這個 FiberNode 就能夠復用,children 是一個文本節點,之后將文本節點更新即可。

多節點 diff

所謂多節點 diff,指的是新節點有多個。

React 團隊發現,在日常開發中,對節點的更新操作的情況往往要多余對節點“新增、刪除、移動”,因此在進行多節點 diff 的時候,React 會進行兩輪遍歷:

  • 第一輪遍歷會嘗試逐個的復用節點
  • 第二輪遍歷處理上一輪遍歷中沒有處理完的節點
第一輪遍歷

第一輪遍歷會從前往后依次進行遍歷,存在三種情況:

  • 如果新舊子節點的key 和 type 都相同,說明可以復用
  • 如果新舊子節點的 key 相同,但是 type 不相同,這個時候就會根據 ReactElement 來生成一個全新的 fiber,舊的 fiber 被放入到 deletions 數組里面,回頭統一刪除。但是注意,此時遍歷并不會終止
  • 如果新舊子節點的 key 和 type 都不相同,或者key不同時,導致不能復用,結束遍歷

示例一

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><div key="e">e</div><div key="d">d</div>
</div>

首先遍歷到 div.key.a,發現該 FiberNode 能夠復用

繼續往后面走,發現 div.key.b 也能夠復用

接下來繼續往后面走,div.key.e,這個時候發現 key 不一樣,因此第一輪遍歷就結束了

示例二

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><p key="c">c</p><div key="d">d</div>
</div>

首先和上面的一樣,div.key.a 和 div.key.b 這兩個 FiberNode 可以進行復用,接下來到了第三個節點,此時會發現 key 是相同的,但是 type 不相同,此時就會將對應的舊的 FiberNode 放入到一個叫 deletions 的數組里面,回頭統一進行刪除,根據新的 React 元素創建一個新的 FiberNode,但是此時的遍歷是不會結束的

接下來繼續往后面進行遍歷,遍歷什么時候結束呢?

  • 到末尾了,也就是說整個遍歷完了
  • 或者是和示例一相同,可以 key 不同

第二輪遍歷

如果第一輪遍歷被提前終止了,那么意味著有新的 React 元素或者舊的 FiberNode 沒有遍歷完,此時就會采用第二輪遍歷

第二輪遍歷會處理這么三種情況:

  • 只剩下舊子節點:將舊的子節點添加到 deletions 數組里面之后統一刪除掉(刪除的情況)
  • 只剩下新的 JSX 元素:根據 ReactElement 元素來創建 FiberNode 節點(新增的情況)
  • 新舊子節點都有剩余:會將剩余的 FiberNode 節點放入一個 map 里面,遍歷剩余的新的 JSX 元素,然后從 map 中去尋找能夠復用的 FiberNode 節點,如果能夠找到,就拿來復用。(移動的情況)
  • 如果不能找到,就新增唄。然后如果剩余的 JSX 元素都遍歷完了,map 結構中還有剩余的 Fiber 節點,就將這些 Fiber 節點添加到 deletions 數組里面,之后統一做刪除操作

只剩下舊子節點

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div>
</div>

遍歷前面兩個節點,發現能夠復用,此時就會復用前面的節點,對于 React 元素來講,遍歷完前面兩個就已經遍歷結束了,因此剩下的FiberNode就會被放入到 deletions 數組里面,之后統一進行刪除

只剩下新的 JSX 元素

更新前

<div><div key="a">a</div><div key="b">b</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

根據新的 React 元素新增對應的 FiberNode 即可。

新舊子節點都有剩余(移動)

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="c">b</div><div key="b">b</div><div key="e">e</div>
</div>

首先會將剩余的舊的 FiberNode 放入到一個 map 里面

接下來會繼續去遍歷剩下的 JSX 對象數組,遍歷的同時,從 map 里面去找有沒有能夠復用

如果在 map 里面沒有找到,那就會新增這個 FiberNode,如果整個 JSX 對象數組遍歷完成后,map 里面還有剩余的 FiberNode,說明這些 FiberNode 是無法進行復用,直接放入到 deletions 數組里面,后期統一進行刪除。

雙端對比算法

所謂雙端,指的是在新舊子節點的數組中,各用兩個指針指向頭尾的節點,在遍歷的過程中,頭尾兩個指針同時向中間靠攏。

因此在新子節點數組中,會有兩個指針,newStartIndex 和 newEndIndex 分別指向新子節點數組的頭和尾。在舊子節點數組中,也會有兩個指針,oldStartIndex 和 oldEndIndex 分別指向舊子節點數組的頭和尾。

每遍歷到一個節點,就嘗試進行雙端比較:「新前 vs 舊前」、「新后 vs 舊后」、「新后 vs 舊前」、「新前 vs 舊后」,如果匹配成功,更新雙端的指針。比如,新舊子節點通過「新前 vs 舊后」匹配成功,那么 newStartIndex += 1,oldEndIndex -= 1。

如果新舊子節點通過「新后 vs 舊前」匹配成功,還需要將「舊前」對應的 DOM 節點插入到「舊后」對應的 DOM 節點之前。如果新舊子節點通過「新前 vs 舊后」匹配成功,還需要將「舊后」對應的 DOM 節點插入到「舊前」對應的 DOM 節點之前。

React不使用雙端算法的原因

實際上在 React 的源碼中,解釋了為什么不使用雙端 diff

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.
}

將上面的注視翻譯成中文如下:

由于雙端 diff 需要向前查找節點,但每個 FiberNode 節點上都沒有反向指針,即前一個 FiberNode 通過 sibling 屬性指向后一個 FiberNode,只能從前往后遍歷,而不能反過來,因此該算法無法通過雙端搜索來進行優化。

React 想看下現在用這種方式能走多遠,如果這種方式不理想,以后再考慮實現雙端 diff。React 認為對于列表反轉和需要進行雙端搜索的場景是少見的,所以在這一版的實現中,先不對 bad case 做額外的優化。

總結

diff 計算發生在更新階段,當第一次渲染完成后,就會產生 Fiber 樹,再次渲染的時候(更新),就會拿新的 JSX 對象(vdom)和舊的 FiberNode 節點進行一個對比,再決定如何來產生新的 FiberNode,它的目標是盡可能的復用已有的 Fiber 節點。這個就是 diff 算法。

在 React 中整個 diff 分為單節點 diff 和多節點 diff。

所謂單節點是指新的節點為單一節點,但是舊節點的數量是不一定的。

單節點 diff 是否能夠復用遵循如下的順序:

  1. 判斷 key 是否相同
  1. 如果更新前后均未設置 key,則 key 均為 null,也屬于相同的情況
  2. 如果 key 相同,進入步驟二
  3. 如果 key 不同,則無需判斷 type,結果為不能復用(有兄弟節點還會去遍歷兄弟節點)
  1. 如果 key 相同,再判斷 type 是否相同
  1. 如果 type 相同,那么就復用
  2. 如果 type 不同,則無法復用(并且兄弟節點也一并標記為刪除)

多節點 diff 會分為兩輪遍歷:

第一輪遍歷會從前往后進行遍歷,存在以下三種情況:

  • 如果新舊子節點的key 和 type 都相同,說明可以復用
  • 如果新舊子節點的 key 相同,但是 type 不相同,這個時候就會根據 ReactElement 來生成一個全新的 fiber,舊的 fiber 被放入到 deletions 數組里面,回頭統一刪除。但是注意,此時遍歷并不會終止
  • 如果新舊子節點的 key 和 type 都不相同,結束遍歷

如果第一輪遍歷被提前終止了,那么意味著還有新的 JSX 元素或者舊的 FiberNode 沒有被遍歷,因此會采用第二輪遍歷去處理。

第二輪遍歷會遇到三種情況:

  • 只剩下舊子節點:將舊的子節點添加到 deletions 數組里面直接刪除掉(刪除的情況)
  • 只剩下新的 JSX 元素:根據 ReactElement 元素來創建 FiberNode 節點(新增的情況)
  • 新舊子節點都有剩余:會將剩余的 FiberNode 節點放入一個 map 里面,遍歷剩余的新的 JSX 元素,然后從 map 中去尋找能夠復用的 FiberNode 節點,如果能夠找到,就拿來復用。(移動的情況)
  • 如果不能找到,就新增唄。然后如果剩余的 JSX 元素都遍歷完了,map 結構中還有剩余的 Fiber 節點,就將這些 Fiber 節點添加到 deletions 數組里面,之后統一做刪除操作

整個 diff 算法最最核心的就是兩個字“復用”。

React 不使用雙端 diff 的原因:

由于雙端 diff 需要向前查找節點,但每個 FiberNode 節點上都沒有反向指針,即前一個 FiberNode 通過 sibling 屬性指向后一個 FiberNode,只能從前往后遍歷,而不能反過來,因此該算法無法通過雙端搜索來進行優化。

React 想看下現在用這種方式能走多遠,如果這種方式不理想,以后再考慮實現雙端 diff。React 認為對于列表反轉和需要進行雙端搜索的場景是少見的,所以在這一版的實現中,先不對 bad case 做額外的優化。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/23895.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/23895.shtml
英文地址,請注明出處:http://en.pswp.cn/web/23895.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++的枚舉

文章目錄 簡介枚舉的基本語法基本使用方法習題簡介 在C++中,枚舉(Enumeration)是一種數據類型,它允許程序員定義一個變量并指定它可以取的那些固定值的集合。枚舉的主要目的是提高代碼的可讀性和維護性,通過使用有意義的名稱而不是數字來表示狀態、類型或其他固定集合的值…

Kafka之Producer原理

1. 生產者發送消息源碼分析 public class SimpleProducer {public static void main(String[] args) {Properties prosnew Properties();pros.put("bootstrap.servers","192.168.8.144:9092,192.168.8.145:9092,192.168.8.146:9092"); // pros.pu…

OceanBase v4.2 特性解析:Lateral Derived Table 優化查詢

前言 從傳統規則來看&#xff0c;內聯視圖通常不允許引用在同一FROM子句中前面定義的表的列。但從OceanBase 4.2.2版本開始&#xff0c;這一限制得到了突破&#xff0c;允許內聯視圖作為Lateral Derived Table來定義&#xff0c;從而允許此類引用。Lateral Derived Table的語法…

26-LINUX--I/O復用-select

一.I/O復用概述 /O復用使得多個程序能夠同時監聽多個文件描述符&#xff0c;對提高程序的性能有很大幫助。以下情況適用于I/O復用技術&#xff1a; ? TCP 服務器同時要處理監聽套接字和連接套接字。 ? 服務器要同時處理 TCP 請求和 UDP 請求。 ? 程序要同時處理多個套接…

Facebook廣告素材如何測試?手把手教你!

廣告素材對Facebook廣告效果的影響是很大的&#xff0c;用對了素材&#xff0c;Facebook廣告的價值就越高。廣告主們通常會先通過廣告測試&#xff0c;根據數據反饋來挑選素材。今天就手把手教你做Facebook素材測試的技巧&#xff0c;讓你更有靈感和思路&#xff01; 創意測試 …

Hudi CLI 安裝配置總結

前言 上篇文章 總結了Spark SQL Rollback, Hudi CLI 也能實現 Rollback,本文總結下 Hudi CLI 安裝配置以及遇到的問題。 官方文檔 https://hudi.apache.org/cn/docs/cli/ 版本 Hudi 0.13.0(發現有bug)、(然后升級)0.14.1Spark 3.2.3打包 mvn clean package -DskipTes…

使用 Django 構建動態網頁

文章目錄 創建 Django 項目和應用程序創建 HTML 模板創建視圖函數配置 URL 路由運行 Django 服務器使用 Django 模板語言 Django 是一個流行的 Python Web 框架&#xff0c;它能夠幫助開發人員快速構建強大的 Web 應用程序。在 Django 中&#xff0c;HTML 是用于呈現網頁內容的…

Spring Boot 復習

2 3 5&#xff08;不考&#xff09; 9 (1)RestController 注解是一個組合注解&#xff0c;等同于Controller 和ResponseBody 兩個注解結合使用的效果。主要作用是將當前類作為控制層的組件添加到 Spring 容器中&#xff0c;同時該類的方法無法返回 JSP 頁面&#xff0c;而且…

Flutter 中的 RenderObjectToWidgetAdapter 小部件:全面指南

Flutter 中的 RenderObjectToWidgetAdapter 小部件&#xff1a;全面指南 Flutter 是一個功能強大的 UI 框架&#xff0c;由 Google 開發&#xff0c;允許開發者使用 Dart 語言構建跨平臺的移動、Web 和桌面應用。在 Flutter 的渲染體系中&#xff0c;RenderObjectToWidgetAdap…

MyBatis面試題系列三

1、#{}和${}的區別是什么&#xff1f; #{}是預編譯處理&#xff0c;${}是字符串替換。 Mybatis 在處理#{}時&#xff0c;會將 sql 中的#{}替換為?號&#xff0c;調用 PreparedStatement 的 set 方法來賦值&#xff1b; Mybatis 在處理${}時&#xff0c;就是把${}替換成變量的值…

SpringBoot項目啟動時“jar中沒有主清單屬性”異常

資料參考 Spring Boot 啟動時 “jar中沒有主清單屬性” 異常 - spring 中文網 (springdoc.cn) 實際解決 更詳細的參考以上&#xff0c;我這邊的話只需要在 pom文件 中加上 spring-boot-maven-plugin 插件就能解決該異常&#xff0c;具體如下&#xff1a; <build><p…

1. 計算機系統概述

1. 計算機系統概述 文章目錄 1. 計算機系統概述1.1 計算機的發展硬件的發展軟件的發展 1.2.1 計算機硬件的基本組成早期馮諾依曼的結構現代計算機的結構 1.2.2 各個硬件的工作原理主存儲器運算器控制器計算機工作過程 1.2.3 計算機系統的多級層次結構1.3 計算機的性能指標存儲器…

GD32如何配置中斷優先級分組以及中斷優先級

使用GD32 MCU的過程中&#xff0c;大家可能會有以下疑問&#xff1a;中斷優先級如何配置和使用&#xff1f; 本文將會為大家解析中斷優先級分組以及中斷優先級的配置使用&#xff1a; 中斷優先級分組配置 一個GD32 MCU系統需要大家明確系統中使用的中斷優先級分組&#xff0…

代駕公司在市場競爭中如何保持優勢?

在競爭激烈的市場中&#xff0c;代駕公司可以通過多種策略保持其競爭優勢&#xff0c;包括利用市場潛力、創新服務模式、提高服務效率以及加強品牌建設等。以下是具體的策略&#xff1a; 利用市場潛力 汽車產業空間巨大&#xff1a;隨著汽車保有量的增加&#xff0c;代駕行業…

掃地機器人:卷價格,不如卷技術

掃地機器人內卷的終點是技術和價值&#xff0c;價格只是附屬品。 一路上漲的價格&#xff0c;一路下跌的銷量 從價格飆升&#xff0c;到重新卷回價格&#xff0c;尷尬的背后是掃地機器人在骨感現實下的無奈抉擇。 根據數據顯示&#xff0c;2020中國掃地機器人線上市場零售均價…

通過可識別性和深度學習重建大腦功能網絡

摘要 本研究提出了一種新的方法來重建代表大腦動力學的功能網絡&#xff0c;該方法基于兩個腦區在同一認知任務中的共同參與會導致其可識別性或其動力學特性降低的觀點。這種可識別性是通過深度學習模型在監督分類任務中獲得的分數來估計的&#xff0c;因此不需要對這種協同參…

零、測試開發前置知識

文章目錄 1、什么是冒煙測試、回歸測試&#xff1f;2、設計測試用例的方法有哪些&#xff1f;3、對于404或500&#xff0c;你會如何分析定位&#xff1f;4、什么是敏捷開發&#xff1f;敏捷開發流程是怎么樣的&#xff1f;5、做接口測試過程中&#xff0c;下游接口需要上游數據…

Flink端到端的精確一次(Exactly-Once)

目錄 狀態一致性 端到端的狀態一致性 端到端精確一次&#xff08;End-To-End Exactly-Once&#xff09; Flink內部的Exactly-Once 輸入端保證 輸出端保證 冪等寫入 事務寫入 Flink和Kafka連接時的精確一次保證 整體介紹 需要的配置 案例 狀態一致性 流式計算本身就…

Java工作學習筆記

1、ConfigurationProperties注解是什么意思&#xff1f; ConfigurationProperties 可以將屬性文件與一個Java類綁定&#xff0c;將屬性文件中的變量值注入到該Java類的成員變量中 示例代碼&#xff1a; /*** SSP配置** author mua*/ Component Data ConfigurationProperties…

如何提高接口響應速度

在非大數據&#xff08;幾萬以上記錄&#xff09;的情況下&#xff0c;影響接口響應速度的因素中最大的是查詢數據庫的次數&#xff0c;其次才是數組遍歷和簡單數據處理&#xff08;如根據已有字段增加新的屬性&#xff0c;或計算值&#xff09;。 一般一次數據庫查詢需要50毫秒…