網站首頁 編程語言 正文
當我們調用 setState 之后發生了什么?react經歷了怎樣的過程將新的 state 渲染到頁面上?
一次react
更新,核心就是對虛擬dom
進行diff
,找出最少的需要變化的dom
節點,然后對其進行相應的dom
操作,用戶即可在頁面上看到更新。但 react 作為廣泛使用的框架,需要考慮更多的因素,考慮多個更新的優先級,考慮主線程占用時長,考慮diff
算法復雜度,考慮性能。。等等,本文就來探討一下react在其內部是如何處理數據更新的。
react在內部使用fiber
這種數據結構來作為虛擬dom【react16+】,它與dom tree
一一對應,形成fiber tree
,一次react更新,本質是fiber tree
結構的更新變化。而fiber tree
結構的更新,用更專業的術語來講,其實就是fiber tree
的協調(Reconcile)。Reconcile
中文意思是調和、使一致,協調fiber tree,就是調整fiber tree
的結構,使其和更新后的jsx
模版結構、dom tree
保持一致。
react從16起,將更新機制分為三個模塊,也可以說是三個步驟,分別是Schedule
【調度】、Reconcile
【協調】、render
【渲染】
Schedule
為什么需要Schedule?
首先我們要知道react在進行協調時,提供了兩種模式:Legacy mode
同步阻塞模式和 Concurrent mode
并行模式。
不同上下文中的更新會觸發不同的模式,如果是在 event
、setTimeout
、network request
的 callback
中觸發更新,react 會采用 Legacy
模式。如果更新與 Suspense
、useTransition
、OffScreen
相關,那么 react 會采用 Concurrent
模式。
Legacy mode
Legacy mode
在協調時會啟動 workLoopSync
。workLoopSync
開始工作以后,要等到所有 fiber node
都處理完畢以后,才會結束工作,也就是 fiber tree
的協調過程不可中斷。
Legacy mode
存在的問題:如果 fiber tree
的結構很復雜,那么協調 fiber tree
可能會占用大量的時間,導致主線程會一直被 js 引擎占用,渲染引擎無法在規定時間(瀏覽器刷新頻率 - 16.7ms)內完成工作,使得頁面出現卡頓(掉幀),影響用戶體驗。
Concurrent mode
鑒于Legacy mode
存在的問題,react團隊在react 16
中提出了 Concurrent mode
的概念,并在react 18
中開放使用。react16、17一直為此做準備。
Concurrent
模式最大的意義在于,使用Concurrent
模式以后的react的應用可以做到:
- 協調可以中斷、恢復;不會長時間阻塞瀏覽器渲染
- 高優先級更新可以中斷低優先級更新,優先渲染
那么,怎么做到這兩點呢?
事實上,Schedule
就是用來完成這個任務的,調度任務的優先級,使高優先級任務優先進入Reconcile
,并且提供中斷和恢復機制。
時間切片
react
采用時間切片的方式來實現協調的中斷和恢復,Concurrent mode
在協調時會啟動 workLoopConcurrent
。 workLoopConcurrent
開始工作以后,每次協調 fiber node
時,都會判斷當前時間片是否到期。如果時間片到期,會停止當前 workLoopConcurrent
,讓出主線程,然后請求下一個時間片繼續協調。
協調的中斷及恢復,類似于瀏覽器的eventloop
,js引擎和渲染引擎互斥,在主線程中交替工作。
我們可以通過模擬 eventLoop
來實現時間分片以及重新請求時間片。一段 js 程序,如果在規定時間內沒有結束,那我們可以主動結束它,然后請求一個新的時間片,在下一個時間片內繼續處理上一次沒有結束的任務。
let taskQueue = []; // 任務列表 let shouldTimeEnd = 5ms; // 一個時間片定義為 5ms let channel = new MessageChannel(); // 創建一個 MessageChannel 實例 function wookLoop() { let beginTime = performance.now(); // 記錄開始時間 while(true) { // 循環處理 taskQueue 中的任務 let currentTime = performance.now(); // 記錄下一個任務開始時的時間 if (currentTime - beginTime >= shouldTimeEnd) break; // 時間片已經到期,結束任務處理 processTask(); // 時間片沒有到期,繼續處理任務 } if (taskQueue.length) { // 時間片到期,通過調用 postMessage,請求下一個時間片 channel.port2.postMessage(null); } } channel.port1.onmessage = wookLoop; // 在下一個時間片內繼續處理任務 workLoop();
和瀏覽器的消息隊列 一樣, react
也會維護一個任務隊列 taskQueue
,然后通過 workLoop
遍歷 taskQueue
,依次處理 taskQueue
中的任務。
taskQueue
中收集任務是有先后處理順序的,workLoop
每次處理 taskQueue
中的任務時,都會挑選優先級最高的任務進行處理。
每觸發一次 react
更新,意味著一次 fiber tree
的協調,但協調并不會在更新觸發時立刻同步進行。相反,react 會為這一次更新,生成一個 task
,并添加到 taskQueue
中,fiber tree
的協調方法會作為新建 task
的 callback
。當 wookLoop
開始處理該 task
時,才會觸發 task
的 callback
,開始 fiber tree
的協調。
任務的優先級
react在內部定義了 5 種類型的優先級,以及對應的超時時間timeout
-
ImmediatePriority
, 直接優先級,對應用戶的click
、input
、focus
等操作;timeout
為 -1,表示任務要盡快處理; -
UserBlockingPriority
,用戶阻塞優先級,對應用戶的mousemove
、scroll
等操作;timeout
為 250 ms; -
NormalPriority
,普通優先級,對應網絡請求、useTransition
等操作;timeout
為 5000 ms; -
LowPriority
,低優先級(未找到應用場景);timeout
為 10000 ms; -
IdlePriority
,空閑優先級,如OffScreen
;timeout
為 1073741823 ms;
5 種優先級的順序為: ImmediatePriority
> UserBlockingPriority
> NormalPriority
> LowPriority
> IdlePriority
。
在確定了任務的優先級以后,react 會根據優先級為任務計算一個過期時間 expirationTime
,即 expirationTime
= currentTime
+ timeout
,然后根據 expirationTime
時間來決定任務處理的先后順序。
expirationTime
越小的任務會被排在task
隊列的越前面,之所以需要timeout
,而不是直接對比優先級等級,是為了避免低優先級任務長時間被 插隊而導致一直無響應;同時,在時間分片到期時,需要根據expirationTime
判斷下一個要處理的任務是否過期,如果已過期,就不能讓出主線程,需要立即處理。
??注:react17中用Lanes重構了優先級算法,此處不展開陳述,有興趣的同學可查閱相關文檔。
獲取最先處理的task
react 采用了小頂堆來存儲task
,實現最小優先隊列,即 taskQueue
是一個小頂堆,放在堆頂的task
是需要最先處理的。
使用最小堆時,有三個操作:push
、pop
、peek
。
push
,入堆操作,即將 task
添加到 taskQueue
中。添加一個新創建的 task
時,會將 task
添加到最小堆的堆底,然后對最小堆做自底向上的調整。調整時,會比較堆節點(task
) 的 expirationTime
,將 expirationTime
較小的 task
向上調整。* peek
,獲取堆頂元素,即獲取需要最先處理的 task
,執行 task
的 callback
,開始 fiber tree
的協調。* pop
,堆頂元素出堆,即 task
處理完畢,從 taskQueue
中移除。移除堆頂元素以后,會對最小堆做自頂向下的調整。調整時,也是比較堆節點(task
) 的 expirationTime
,將 expirationTime
較大的 task
向下調整。### 高優先級的更新中斷低優先級的更新
Concurrent
模式下,如果在低優先級更新的協調過程中,有高優先級更新進來,那么高優先級更新會中斷低優先級更新的協調過程。
每次拿到新的時間片以后,workLoopConcurrent
都會判斷本次協調對應的優先級和上一次時間片到期中斷的協調的優先級是否一樣。如果一樣,說明沒有更高優先級的更新產生,可以繼續上次未完成的協調;如果不一樣,說明有更高優先級的更新進來,此時要清空之前已開始的協調過程,從根節點開始重新協調。等高優先級更新處理完成以后,再次從根節點開始處理低優先級更新。
Reconcile
前面說到,reconcile
(協調)就是fiber tree
結構的更新,那么具體是怎樣更新的呢?本小節就來解答這個問題。
前置知識
從jsx到dom
Step1: 從jsx
生成react element
:
jsx
模板通過 babel
編譯為 createElement
方法;執行組件方法,觸發 createElement
的執行,返回 react element
;
Step2: 從react element
生成fiber tree
:
-
fiber tree
中存在三種類型的指針child
、sibling
、return
。其中,child
指向第一個子節點,sibling
指向兄弟節點,return
指針指向父節點;*fiber tree
采用的深度優先遍歷,如果節點有子節點,先遍歷子節點;子節點遍歷結束以后,再遍歷兄弟節點;沒有子節點、兄弟節點,就返回父節點,遍歷父節點的兄弟節點;* 當節點的return
指針返回null
時,fiber tree
的遍歷結束;Step3:fiber tree
生成之后,從fiber tree
到真實dom
,就是處理fiber tree
上對應的副作用,包括: - 所有
dom
節點的新增; -
componentDidMount
、useEffect
的callback
函數的觸發; -
ref
引用的初始化;
雙緩存fiber tree
react 做更新處理時,會同時存在兩顆 fiber tree
。一顆是已經存在的 old fiber tree
,對應當前屏幕顯示的內容,稱為 current fiber tree
;另外一顆是更新過程中構建的 new fiber tree
,稱為 workInProgress fiber tree
。
current fiber tree
和workInProgress fiber tree
可以通過alternate
指針互相訪問
當更新完成以后,使用 workInProgress fiber tree
替換掉 current fiber tree
,作為下一次更新的 current fiber tree
。
協調的過程
協調過程中主要做三件事情:
1.為 workInProgress fiber tree
生成 fiber node
;
2.為發生變化的 fiber node
標記副作用 effect
;
3.收集帶 effect
的 fiber node
;
生成workInProgress fiber tree
workInProgress fiber tree
作為一顆新樹,生成 fiber node
的方式有三種:
- 克隆(淺拷貝)
current fiber node
,意味著原來的dom
節點可以復用,只需要更新dom
節點的屬性,或者移動dom
節點; - 新建一個
fiber node
,意味著需要新增加一個dom
節點; - 直接復用
current fiber node
,表示對應的dom
節點完全不用做任何處理;
復用的場景:當子組件的渲染方法(類組件的 render
、函數組件方法)沒有觸發,(比如使用了React.memo
),沒有返回新的 react element
,子節點就可以直接復用 current fiber node
。
在日常開發過程中,我們可以通過合理使用 ShouldComponentUpdate
、React.memo
,阻止不必要的組件重新 render
,通過直接復用 current fiber node
,加快 workInProgress fiber tree
的協調,達到優化的目的。
相反,只要組件的渲染方法被觸發,返回新的 react element
,那么就需要根據新的 react element
為子節點創建 fiber node
(通過淺拷貝或新建)。
- 如果能在
current fiber tree
中找到匹配節點,那么可以通過克隆(淺拷貝)current fiber node
的方式來創建新的節點; - 相反,如果無法在
current fiber tree
找到匹配節點,那么就需要重新創建一個新的節點;
我們常說的diff算法
就是發生在這一環節。
diff算法
比較的雙方是 workInProgress fiber tree
中用于構建 fiber node
的 react element
和 current fiber tree
中的 fiber node
,比較兩者的 key
和 type
,根據比較結果來決定如何為 workInProgress fiber tree
創建 fiber node
。
【 key 和 type 】:
key
就是 jsx
模板中元素上的 key
屬性。如果不寫默認為undefined
。jsx
模板轉化為 react element
后,元素的 key
屬性會作為 react element
的 key
屬性。同樣的,react element
轉化為 fiber node
以后,react element
的 key
屬性也會作為 fiber node
的 key
屬性。
jsx
中不同的元素類型,有不同的type
:
<Component name="xxxx" /> //type = Component, 是一個函數 <div></div> // type = "div", 是一個字符串 <React.Fragment></React.Fragment> // type = React.Fragment, 是一個數字(react 內部定義的);
jsx
模板轉化為 react element
以后,react element
的 type
屬性會根據 jsx
元素的類型賦不同的值,可能是組件函數,也可能是 dom
標簽字符串,還可能是數字。 react element
轉化為 fiber node
以后,react element
的 type
屬性也會作為 fiber node
的 type
屬性。
綜上,判斷拷貝 current fiber node
的邏輯,概括來就是:
reactElement.key === currentFiberNode.key && reactElement.type === currentFiberNode.type, current fiber node //可以克隆; reactElement.key !== currentFiberNode.key, current fiber node //不可克隆; reactElement.key === currentFiberNode.key && reactElement.type !== currentFiberNode.type, current fiber node //不可克隆;
diff 算法:
- 已匹配的父節點的直接子節點進行比較,不跨父節點比較;
- 通過比較
key
、type
來判斷是否需要克隆current fiber node
。只有key
和type
都相等,才克隆current fiber node
作為新的節點,否則就需要新建一個節點。key
值和節點類型type
,key
的優先級更高。如果key
值不相同,那么節點不可克隆。 - 當比較
single react element
和current fiber node list
時,只需要遍歷current fiber node list
,比較每個current fiber node
和react element
的key
值和type
。只有key
和type
都相等,react element
和current fiber node
才能匹配。如果有匹配的,直接克隆current fiber node
,作為react element
對應的workInProgress fiber node
。如果沒有匹配的current fiber node
,就需要為react element
重新創建一個新的fiber node
作為workInProgress fiber node
。 - 當比較
react element list
和current fiber node list
時,還需要通過列表下標index
判斷wokrInProgress fiber node
是否相對于克隆的current fiber node
發生了移動。這也是diff
中最復雜的地方。
為發生變化的fiber node標記effect
判斷節點是否發生變化
- 節點只要是重新創建的而不是克隆自
current fiber node
,那么節點就百分之百發生了變化,需要更新;* 節點克隆自current fiber node
,需要比較props
是否發生了變化,如果props
發生了變化,節點需要更新;* 節點克隆自current fiber node
,且是組件類型,還需要比較state
是否發生了變化,如果state
發生了變化,節點需要更新;常見的effect
類型: -
Placement
,放置,只針對dom
類型的fiber node
,表示節點需要做移動或者添加操作。 -
Update
,更新,針對所有類型的fiber node
,表示fiber node
需要做更新操作。 -
PlacementAndUpdate
,放置并更新,只針對dom
類型的fiber node
,表示節點發生了移動且props
發生了變化。 -
Ref
,表示節點存在ref
,需要初始化 / 更新ref.current
。 -
Deletion
,刪除,針對所有類型的fiber node
,表示fiber node
需要移除。 -
Snapshot
,快照,主要是針對類組件fiber node
。當類組件fiber node
發生了mount
或者update
操作,且定義了getSnapshotBeforeUpdate
方法,就會標記Snapshot
。 -
Passive
,主要針對函數組件fiber node
,表示函數組件使用了useEffect
。當函數組件節點發生mount
或者update
操作,且使用了useEffect hook
,就會給fiber node
標記Passive
。 -
Layout
,主要針對函數組件fiber node
,表示函數組件使用了useLayoutEffect
。當函數組件節點發生mount
或者update
操作,且使用了useLayoutEffect hook
,就會給fiber node
標記Layout
。
react 使用二進制數來聲明 effect
,如 Placement
為 2 (0000 0010),Update
為 4 (0000 0100)。一個 fiber node
可同時標記多個 effect
,如函數組件 props
發生變化且使用了 useEffect hook
,那么就可以使用 Placement | Update = 516(位運算符)
來標記。
收集帶effect的fiber node
如果一個 fiber node
被標記了 effect
,那么 react
就會在這個 fiber node
完成協調以后,將這個 fiber node
收集到effectList
中。當整顆 fiber tree
完成協調以后,所有被標記 effect
的 fiber node
都被收集到一起。
收集fiber node
的 effectList
采用單鏈表結構存儲,firstEffect
指向第一個標記 effect
的 fiber node
,lastEffect
標記最后一個 fiber node
,節點之間通過 nextEffect
指針連接。
由于 fiber tree
協調時采用的順序是深度優先,協調完成的順序是子節點、子節點兄弟節點、父節點,所以收集帶 effect
標記的 fiber node
時,順序也是子節點、子節點兄弟節點、父節點。
Render
render
也稱為commit
,是對協調過程中標記的effect
的處理
effect
的處理分為三個階段,這三個階段按照從前到后的順序為:
1.before mutation
階段 (dom
操作之前)
2.mutation
階段 (dom
操作)
3.layout
階段 (dom
操作之后)
不同的階段,處理的 effect
種類也不相同。在每個階段,react 都會從 effectList
鏈表的頭部 - firstEffect
開始,按序遍歷 fiber node
, 直到 lastEffect
。
before mutation階段
before mutation
階段的主要工作是處理帶 Snapshot
標記的 fiber node
。 從 firstEffect
開始遍歷 effect
列表,如果 fiber node
帶 Snapshot
標記,觸發 getSnapshotBeforeUpdate
方法。
mutation階段
mutation
階段的主要工作是處理帶 Deletion
、 Placement
、PlacementAndUpdate
、Update
標記的 fiber node
。 在這一階段,涉及到 dom
節點的更新、新增、移動、刪除,組件節點刪除導致的 componentWillUnmount
、destory
方法的觸發,以及刪除節點引發的 ref
引用的重置。
dom
節點的更新:
- 通過原生的 API
setAttribute
、removeArrribute
修改dom
節點的attr
; - 直接修改
dom
節點的style
; - 直接修改
dom
節點的innerHtml
、textContent
;
dom
節點的新增和移動:
- 如果新增(移動)的節點是父節點的最后一個子節點,那么可以直接使用
appendChild
方法。 - 如果不是最后一個節點,需要使用
insertBefore
方法。通過遍歷找到第一個沒有帶Placement
標記的節點作為insertBefore
的定位元素。
dom
節點的刪除:
- 如果節點是
dom
節點,通過removeChild
移除; - 如果節點是組件節點,觸發
componentWillUnmount
、useEffect
的destory
方法的執行; - 如果標記
Deletion
的節點的子節點中有組件節點,深度優先遍歷子節點,依次觸發子節點的componentWillUnmount
、useEffect
的destory
方法的執行; - 如果標記
Deletion
的節點及子節點關聯了ref
引用,要將ref
引用置空,及ref.current
=null
(也是深度優先遍歷);
layout 階段
layout
階段的主要工作是處理帶 update
標記的組件節點和帶 ref
標記的所有節點。 工作內容如下:
- 如果類組件節點是
mount
操作,觸發componentDidMount
;如果是update
操作,觸發componentDidUpdate
; - 如果函數組件節點時
mount
操作,觸發useLayoutEffect
的callback
;如果是update
操作,先觸發上一次更新生成的destory
,再觸發這一次的callback
; - 異步調度函數組件的
useEffect
; - 如果組件節點關聯了
ref
引用,要初始化ref.current
;
原文鏈接:https://blog.csdn.net/web22050702/article/details/128398129
相關推薦
- 2022-04-28 Shell?命令啟動Docker?Container的實現_linux shell
- 2022-03-23 Android?Camera2開啟自動曝光功能_Android
- 2023-03-28 Unity3D開發之獲取所有的子對象的方法詳解_C#教程
- 2022-04-21 Docker容器跨主機通信overlay網絡的解決方案_docker
- 2022-12-06 React?Redux應用示例詳解_React
- 2022-06-20 基于python?的Pygame最小開發框架_python
- 2022-12-31 go操作Kafka使用示例詳解_Golang
- 2022-06-17 教你Docker安裝GitLab功能_docker
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支