React 源碼揭秘 | 更新隊列

前面幾篇遇到updateQueue的時候,我們把它先簡單的當成了一個隊列處理,這篇我們來詳細討論一下這個更新隊列。 有關updateQueue中的部分,可以見源碼??UpdateQueue實現

Update對象

我們先來看一下UpdateQueue中的內容,Update對象,其實現如下:

/** 更新的Action 可以是State 也可以是函數 */
export type Action<State> = State | ((prevState: State) => State);
/** 定義Dispatch函數 */
export type Dispatch<State> = (action: Action<State>) => void;/** 更新對象 */
export class Update<State> {next: Update<State>;action: Action<State>;lane: Lane; // 當前更新的優先級Laneconstructor(action: Action<State>, lane: Lane) {this.action = action;this.next = null;this.lane = lane;}
}

其中,包含

  • action: Action對象,可以是任意類型,對應的我們在setState中傳入的參數,如果傳入一個函數,對應的是函數類型action,則運行函數得到狀態值。如果不是函數,則直接將其作為狀態值。
  • lane: 當前更新對應的優先級lane
  • next: 涉及到updateQueue的數據結構,指向下一個Update對象?

我們在很多地方都需要創建更新對象,比如dispatchSetState是,即你修改狀態的時候

?初始化的時候,在updateContainer中,也會創建update對象

updateQueue - 環形鏈表?

updateQueue本質上是一個存儲Update對象的數據結構,但是其不是一個普通的數組,其內部實現了一個環形鏈表用來存儲Update對象,其定義如下

export class UpdateQueue<State> {shared: {pending: Update<State> | null;};/** 派發函數 */dispatch: Dispatch<State>;/** 基礎隊列 */baseQueue: Update<State> | null;/** 基礎state */baseState: State;
...
}

其內部包含shared屬性,指向一個對象,對象中包含pending對象,指向Update對象,如下圖所示

其中,Update對象的next指針指向下一個Update對象,其組成一個環形鏈表,如圖所示:

其中:

  • updateQueue.shared.pending指向最后一個Update節點
  • updateQueue.shared.pending.next 為第一個Update節點?

?為什么使用環形鏈表?

這里使用環形鏈表的一個好處是,其可以很方便的找到首位元素,可以方便的遍歷鏈表,也可以方便的對兩個鏈表進行拼接,這個在后面的baseQueue 和 baseState邏輯中會用到。

?enqueue入隊

enqueue為UpdateQueue的類方法,其作用就是給隊列插入Update對象,其實現如下:

 /** 入隊,構造環狀鏈表 */enqueue(update: Update<State>, fiber: FiberNode, lane: Lane) {if (this.shared.pending === null) {// 插入第一個元素,此時的結構為// shared.pending -> firstUpdate.next -> firstUpdateupdate.next = update;this.shared.pending = update;} else {// 插入第二個元素update.next = this.shared.pending.next;this.shared.pending.next = update;this.shared.pending = update;}/** 在當前的fiber上設置lane */fiber.lanes = mergeLane(fiber.lanes, lane);/** 在current上也設置lane 因為在beginwork階段 wip.lane = NoLane 如果bailout 需要從current恢復 */const current = fiber.alternate;if (current) {current.lanes = mergeLane(current.lanes, lane);}}

我們用一個插入隊列來演示插入過程:

// 假設有插入隊列
enqueue(100)
enqueue(current => current + 1)
enqueue(200)

插入100, 100對應的pending.next指向自己,此時100對應的Update又是首節點也是尾節點

插入curr=>curr+1的update節點,此時首節點為pending.nexy也就是 curr=>curr+1 尾節點為100

插入200節點,此時首節點為200 尾節點為100 都是從pending.next的位置插入,如圖

設置lane

enqueue方法除了傳入更新對象,還需要傳入更新所發生在的Fiber對象和對應的更新lane,其目的是在當前更新的Fiber上記錄lane,其邏輯如下:

    /** 在當前的fiber上設置lane */fiber.lanes = mergeLane(fiber.lanes, lane);/** 在current上也設置lane 因為在beginwork階段 wip.lane = NoLane 如果bailout 需要從current恢復 */const current = fiber.alternate;if (current) {current.lanes = mergeLane(current.lanes, lane);}

可以看到,當前更新的fiber節點的alternate節點的lanes也被設置了,這是為了先保存當前的lanes方便后面中短渲染 如bailout的時候能恢復當前fiber的lanes

processQueue - 處理更新

process函數的作用就是處理當前隊列的所有更新,在不考慮優先級的情況下,其實現可以簡化為如下代碼:

  /** 處理任務 */process() {// 當前遍歷到的updatelet memorizedState;let currentUpdate = this.baseQueue?.next;if (currentUpdate) {do {const currentAction = currentUpdate.action;if (currentAction instanceof Function) {/** Action是函數類型 運行返回newState */memorizedState = currentAction(memorizedState);} else {/** 非函數類型,直接賦給新的state */memorizedState = currentAction;}currentUpdate = currentUpdate.next;} while (currentUpdate !== this.baseQueue?.next);}return  memorizedState;}

即循環遍歷整個環狀鏈表,對action的類型進行檢測,如果是函數則運行,如果是非函數直接把ation賦給memorizedState,最后將memorizedState返回即可!?

引入優先級lane

如果加入優先級lane的處理邏輯,process的處理邏輯會稍微有些復雜,我們看個例子

onClick={()=>{// 同步更新Lane = 1setvariable(100)startTransition(()=>{// 可以理解為 創建一個優先級lane=8的UpdatesetVariable(curr=>curr+100)    })// 同步更新Lane = 1setVariable(curr => curr + 100)}}

在一個onClick函數中,我們設置了三次setVariable函數,其中,第二次setter使用startTranstion包裹,這個函數由useTranstion hook提供,這個后面再講,你可以先理解為,在這個startTransition包裹的setter對應的優先級都會被改成 8 即可 TransitionLane

此時,variable hook中的updateQueue對應的shared.pending隊列如下:

由于隊列中的優先級不同,我們一次只處理一個優先級的Update對象,對于其他優先級的對象需要進行跳過。

但是需要注意,被我們跳過的更新需要在后面的更新中被執行,并且,雖然我們通過優先級把一次更新拆分成了兩次更新,但是最終的結果需要是一樣的。

比如,第一次更新?

執行 action 100

跳過 curr=>curr+100 并且記住此時的狀態100

執行curr => curr + 200

此時的結果為 300

第二次更新,需要從上次執行到的位置重新執行

執行curr=>curr+100 結果為200

執行 curr=>curr+200 (雖然此Update執行過了,但是為了保證結果一致,還需執行)結果為400

注意,雖然拆成了兩次更新,但是最終更新的結果一定是和不加startTranstion按順序執行的結果一樣的!

這樣我們就可以把高耗時的更新操作設置低優先級,先處理低耗時的更新,同時保證最終結果不變。

實現這樣邏輯的算法如下:

準備一個memorizedState,記錄當前updateQueue的狀態值

準備一個baseState 用來記錄第一個 跳過第一個Update時的狀態值

準備一個baseQueue,用來記錄本次更新跳過的更新對象?和 跳過更新之后的更新對象, 下一次更新就用這個baseQueue中的Update

遍歷隊列元素,使用isSubsetOfLanes來判斷當前Update.lane是不是等于當前正在更新的lane(wipRenderedLane)

如果是則看baseQueue隊列

? ?如果baseQueue隊列為空,?則執行action,給memorizedState賦值

? ?如果baseQueue隊列不為空, 則說明當前更新前面,已經有跳過的Update被加入到baseQueue了,那么其后面所有的Update對象都要加入baseQueue,則把當前Update對象克隆一份,并且設置優先級為Nolanes,以保證下次更細當前Update一定能被執行,推入baseQueue

并且,由于當前Update的lane是滿足的,需要執行action,更新memorizedState

如果不是, 看updateQueue隊列

?如果隊列為空,此時為第一個跳過的Update對象,把當前的Update對象克隆一 份push到baseQueue中,并且把當前memorizedState賦給baseState,記錄本次更新第一個跳過Update對應的狀態,下次更新就從此開始

如果隊列不為空,和上面一樣,區別就是不賦baseState了,注意baseState只有第一次更新才設置

最后返回 memorizedState 并且把baseState baseQueue記錄在當前updateQueue對象上,復習一下UpdateQueue的ts定義。

export class UpdateQueue<State> {shared: {pending: Update<State> | null;};/** 派發函數 */dispatch: Dispatch<State>;/** 基礎隊列 */baseQueue: Update<State> | null;/** 基礎state */baseState: State;
...
}

下面我們畫圖來解釋一下 Update隊列如下:

Update List 
[action: 100,lane: 1]
[action: curr => curr + 100, lane: 8]
[action: curr = curr+ 200,lane: 1]

此時的updateQueue和狀態如下:?

此時的root.pendinglanes 包含lane1 和 lane8 即SyncLane和TranstionLane

開始更新最高的優先級lane1?, 處理第一個Update,由于滿足優先級,直接計算并且更新memorizedState = 100

繼續處理到curr=>curr+100 此時lane=8 需要跳過,但是此時baseQueue為空,為第一個跳過的更新,需要baseState記錄跳過之前的memorizedState = 100,并且克隆一份Update 推入baseQueue

?

繼續處理curr=>curr+200 此時滿足lane=1 但是由于baseQueue已經不為空,則后面所有的Update無論什么優先級,都需要克隆一份Update對象并且設置lanes為NoLane 推入baseQueue

同時需要計算action更新memorizedState為300

?

第一輪更新結束,此時狀態為300,保存baseState和baseQueue并且刪除shared.pending隊列,因為已經用不上了。

第二輪更新 lane=8 此時從baseQueue中取出上次跳過的更新,繼續處理,此時memorizedState被baseState初始化為100

?處理第一個更新,此時memorizedState=200

處理第二個更新,由于是任意Lanes&NoLanes === NoLanes 所以第二個update也滿足優先級,更新memorizedState=400 此時完成更新?

?最終結果為400

兩次更新,第一次更新值為300 第二次更新值為400 做到了過渡的作用

如果頁面中包含邏輯,如果variable === 400 則渲染10000個li 此時如果不用startTranstion降低優先級,則更新variable到400的那次更新的優先級lane=1 那么此時如果有更高優先級任務來,則此次lane=1的更新無法被打斷,導致頁面卡住不動 影響用戶體驗。

如果更新到400的更新優先級為8 那么當更高優先級更新來的時候,此次大規模的更新會被打斷,優先執行更高優先級更新(比如用戶事件) 在高優先級任務執行完成之后,再執行這個大規模更新渲染,優化了用戶體驗!

連接baseQueue和pending

每一輪更新之后,pending對應的update環會被清空,但是當處理本次更新的時候,又有新的update被掛上,此時baseQueue和pending都有值

比如,在某次更新的useEffect中,設置了setVariable 此時的更新隊列中又有新的更新了

此時就需要把baseQueue隊列和pending隊列連接,baseQueue隊列在前

需要定義兩個變量 baseFirst 和 pendingFirst 分別指向baseQueue和pending的對頭,因為改變過pending/baseQueue.next 之后 就無法直接找到隊頭元素

第一步 設置baseQueue.next = pendingFirst 把baseQueue尾和pending頭連接 如圖

?第二步?Pending.next = baseFirst 此時pending隊列的尾和baseQueue頭連接 如圖

此時 baseFirst 就是整個隊列的頭部了

說完了原理,我們看一下process方法的完整實現:

  /** 處理任務 */process(renderLane: Lane, onSkipUpdate?: (update: Update<any>) => void) {/** 獲取baseQueue pending 完成拼接 */let baseState = this.baseState;let baseQueue = this.baseQueue;const currentPending = this.shared.pending;// 生成新的baseQueue過程if (currentPending !== null) {if (baseQueue !== null) {// 拼接兩個隊列// pending -> p1 -> p2 -> p3const pendingFirst = currentPending.next; // p1// baseQueue -> b1->b2->b3const baseFirst = baseQueue.next; // b1// 拼接currentPending.next = baseFirst; // p1 -> p2 -> p3 -> pending -> b1 -> b2 -> b3baseQueue.next = pendingFirst; //b1-> b2 -> b3 -> baseQueue -> p1 -> p2 -> p3// p1 -> p2 -> p3 -> pending -> b1 -> b2 -> b3 baseQueue}// 合并 此時 baseQueue -> b1 -> b2 -> b3 -> p1 -> p2 -> p3baseQueue = currentPending;// 覆蓋新的baseQueuethis.baseQueue = baseQueue;// pending可以置空了this.shared.pending = null;}// 消費baseQueue過程// 設置新的basestate和basequeuelet newBaseState: State = baseState;let newBaseQueueFirst: Update<State> | null = null;let newBaseQueueLast: Update<State> | null = null;// 新的計算值let memorizedState: State = baseState;// 當前遍歷到的updatelet currentUpdate = this.baseQueue?.next;if (currentUpdate) {do {const currentUpdateLane = currentUpdate.lane;// 看是否有權限if (isSubsetOfLanes(renderLane, currentUpdateLane)) {// 有權限if (newBaseQueueFirst !== null) {// 已經存在newBaseFirst 則往后加此次的update 并且將此次update的lane設置為NoLane 保證下次一定能運行const clone = new Update(currentUpdate.action, NoLane);newBaseQueueLast = newBaseQueueLast.next = clone;}if (currentUpdate.hasEagerState) {memorizedState = currentUpdate.eagerState;} else {// 不論存不存在newBaseFirst 都要計算memorizedStateconst currentAction = currentUpdate.action;if (currentAction instanceof Function) {/** Action是函數類型 運行返回newState */memorizedState = currentAction(memorizedState);} else {/** 非函數類型,直接賦給新的state */memorizedState = currentAction;}}} else {// 無權限const clone = new Update(currentUpdate.action, currentUpdate.lane);if (onSkipUpdate) {onSkipUpdate(clone);}// 如果newBaseQueueFirst === null 則從第一個開始添加newbaseQueue隊列if (newBaseQueueFirst === null) {newBaseQueueFirst = newBaseQueueLast = clone;// newBaseState到此 不在往后更新 下次從此開始newBaseState = memorizedState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}}currentUpdate = currentUpdate.next;} while (currentUpdate !== this.baseQueue?.next);}if (newBaseQueueFirst === null) {// 此次沒有update被跳過,更新newBaseStatenewBaseState = memorizedState;} else {// newbaseState不變 newBaseQueueFirst newBaseQueueLast 成環newBaseQueueLast.next = newBaseQueueFirst;}// 保存baseState和BaseQueuethis.baseQueue = newBaseQueueLast;this.baseState = newBaseState;return { memorizedState };}

?

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

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

相關文章

[SQL] 事務的四大特性(ACID)

&#x1f384;事務的四大特性 以下就是事務的四大特性&#xff0c;簡稱ACID。 原子性&#x1f4e2;事務時不可分割的最小操作單元&#xff0c;要么全部成功&#xff0c;要么全部失敗。一致性&#x1f4e2;事務完成后&#xff0c;必須使所有的數據都保持一致隔離性&#x1f4e2…

DeepSeek 提示詞:基礎結構

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家&#xff0c;歷代文學網&#xff08;PC端可以訪問&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移動端可微信小程序搜索“歷代文學”&#xff09;總架構師&#xff0c;15年工作經驗&#xff0c;精通Java編…

如何使用 Python 連接 MySQL 數據庫?

在Python開發中&#xff0c;連接MySQL數據庫是一個常見的需求。 我們可以使用多種庫來實現這一功能&#xff0c;其中最常用的是mysql-connector-python和PyMySQL。 下面我將詳細介紹如何使用這兩個庫來連接MySQL數據庫&#xff0c;并提供一些實際開發中的建議和注意事項。 1…

Apache DolphinScheduler系列1-單節點部署及測試報告

文章目錄 整體說明一、部署環境二、版本號三、部署方案四、部署步驟4.1、上傳部署包4.2、創建外部數據庫4.3、修改元數據庫配置4.4、上傳MySQLl驅動程序4.5、初始化外部數據庫4.6、啟停服務4.7、訪問頁面五、常見問題及解決方式5.1、時間不一致5.2、異常終止5.3、大量日志5.4、…

LLM之論文閱讀——Context Size對RAG的影響

前言 RAG 系統已經在多個行業中得到廣泛應用&#xff0c;尤其是在企業內部文檔查詢等場景中。盡管 RAG 系統的應用日益廣泛&#xff0c;關于其最佳配置的研究卻相對缺乏&#xff0c;特別是在上下文大小、基礎 LLM 選擇以及檢索方法等方面。 論文原文: On the Influence of Co…

人工智能(AI):科技新紀元的領航者

摘要 人工智能&#xff08;AI&#xff09;作為當今科技領域最具變革性的力量之一&#xff0c;正以驚人的速度重塑著我們的世界。本文旨在全面且專業地介紹人工智能&#xff0c;涵蓋其定義、發展歷程、關鍵技術、應用領域、面臨的挑戰以及未來展望等方面&#xff0c;以期為讀者…

如何防止 Docker 注入了惡意腳本

根據您的描述&#xff0c;攻擊者通過 CentOS 7 系統中的 Docker 注入了惡意腳本&#xff0c;導致自動啟動名為 “masscan” 和 “x86botnigletjsw” 的進程。這些進程可能用于網絡掃描或其他惡意活動。為了解決這一問題&#xff0c;建議您采取以下步驟&#xff1a; 1. 停止并刪…

LLaMA-Factory|微調大語言模型初探索(4),64G顯存微調13b模型

上篇文章記錄了使用lora微調deepseek-7b&#xff0c;微調成功&#xff0c;但是微調llama3-8b顯存爆炸&#xff0c;這次嘗試使用qlora微調HQQ方式量化&#xff0c;微調更大參數體量的大語言模型&#xff0c;記錄下來微調過程&#xff0c;僅供參考。 對過程不感興趣的兄弟們可以直…

詳解 Spring 配置數據源的兩種方式

在 Spring 框架中配置數據源&#xff08;DataSource&#xff09;主要有兩種方式&#xff1a; 通過 Setter 注入配置數據源通過 jdbc.properties 配置文件方式 本博文將使用 Druid 作為數據源&#xff0c;其在 Spring 項目中常見且高效。 Druid 被廣泛認為是性能最佳的連接池…

項目進度管理工具:甘特圖與關鍵路徑法(2025實戰指南)

在全球數字化轉型加速的背景下&#xff0c;項目延期率高達42%的現狀倒逼管理者掌握科學的進度管理工具。本文結合2025年最新實踐&#xff0c;深度解析甘特圖與關鍵路徑法的原理及應用&#xff0c;助你構建精準可控的項目進度管理體系。 一、雙劍合璧&#xff1a;工具組合的價值…

RAGS評測后的數據 如何利用influxdb和grafan 進行數據匯總查看

RAGS(通常指相關性、準確性、語法、流暢性)評測后的數據能借助 InfluxDB 存儲,再利用 Grafana 進行可視化展示,實現從四個維度查看數據,并詳細呈現每個問題對應的這四個指標情況。以下是詳細步驟: 1. 環境準備 InfluxDB 安裝與配置 依據自身操作系統,從 InfluxDB 官網下…

詳解Redis如何持久化

引言 本文介紹了 Redis 的兩種持久化方式&#xff1a;RDB 和 AOF。RDB 按時間間隔快照存儲&#xff0c;AOF 記錄寫操作。闡述了它們的配置、工作原理、恢復數據的方法、性能與實踐建議&#xff0c;如降低 fork 頻率、控制內存等&#xff0c;還提到二者可配合使用&#xff0c;最…

HarmonyOS Design 介紹

HarmonyOS Design 介紹 文章目錄 HarmonyOS Design 介紹一、HarmonyOS Design 是什么&#xff1f;1. 設計系統&#xff08;Design System&#xff09;2. UI 框架的支持3. 設計工具和資源4. 開發指南5. 與其他設計系統的對比總結 二、HarmonyOS Design 特點 | 應用場景1. Harmon…

PC端-發票真偽查驗系統-Node.js全國發票查詢接口

在現代企業的財務管理中&#xff0c;發票真偽的驗證至關重要。隨著電子發票的普及&#xff0c;假發票問題日益嚴峻&#xff0c;如何高效、準確的對發票進行真偽查驗&#xff0c;已經成為各類企業在日常運營中必須解決的關鍵問題。翔云發票查驗接口做企業財務管理、稅務合規的好…

Java 大視界 -- 基于 Java 的大數據機器學習模型壓縮與部署優化(99)

&#x1f496;親愛的朋友們&#xff0c;熱烈歡迎來到 青云交的博客&#xff01;能與諸位在此相逢&#xff0c;我倍感榮幸。在這飛速更迭的時代&#xff0c;我們都渴望一方心靈凈土&#xff0c;而 我的博客 正是這樣溫暖的所在。這里為你呈上趣味與實用兼具的知識&#xff0c;也…

算法-圖-數據結構(鄰接矩陣)-BFS廣度優先遍歷

鄰接矩陣廣度優先遍歷&#xff08;BFS&#xff09;是一種用于遍歷或搜索圖的算法&#xff0c;以下是具體介紹&#xff1a; 1. 基本概念 圖是一種非線性的數據結構&#xff0c;由頂點和邊組成&#xff0c;可分為無向圖、有向圖、加權圖、無權圖等。鄰接矩陣是表示圖的一種數…

【HDLbits--Comb組合邏輯】

HDLbits--Comb組合邏輯 1.5 組合邏輯1.5 Demo 在 Verilog 中&#xff0c;組合邏輯&#xff08;Combinational Logic&#xff09;是指輸出僅依賴于當前輸入的邏輯電路&#xff0c;沒有記憶功能&#xff08;即沒有狀態存儲&#xff09;。組合邏輯的特點是&#xff1a; 無時鐘信號…

ARM Cortex-M3 技術解析:核寄存器R1-R15介紹及使用

ARM Cortex-M3 技術解析&#xff1a;核寄存器R1-R15介紹及使用 作為嵌入式開發領域的經典處理器內核&#xff0c;ARM Cortex-M3&#xff08;CM3&#xff09;憑借其高效能、低功耗和豐富特性&#xff0c;在工業控制、物聯網、消費電子等領域廣泛應用。而內核寄存器是我們調試代…

python unzip file

要在 Python 中解壓文件并顯示進度&#xff0c;我們需要在解壓過程中跟蹤文件的提取進度。由于 zipfile 模塊本身不直接支持進度顯示&#xff0c;我們可以通過手動計算并使用 tqdm 庫來顯示進度條。 安裝 tqdm 首先&#xff0c;確保你已經安裝了 tqdm 庫&#xff0c;用于顯示…

DeepSeek+Kimi生成高質量PPT

DeepSeek與Kimi生成PPT全流程解析 一、工具分工原理 DeepSeek核心作用&#xff1a;生成結構化PPT大綱&#xff08;擅長邏輯構建與內容優化&#xff09;Kimi核心作用&#xff1a;將文本轉換為視覺化PPT&#xff08;提供模板庫與排版引擎&#xff09; 二、操作步驟詳解 1. 通…