網站首頁 編程語言 正文
setState執行之后會發生什么
setState
執行之后,會執行一個叫 enqueueSetState
的方法,這個主要作用是創建 Update
對象和發起調度,可以看下這個函數的邏輯,
enqueueSetState: function (inst, payload, callback) { // 1. inst是組件實例,從組件實例中拿到當前組件的Fiber節點 var fiber = get(inst); var eventTime = requestEventTime(); var lane = requestUpdateLane(fiber); // 2.1 根據更新發起時間、優先級、更新的payload創建一個update對象 var update = createUpdate(eventTime, lane); update.payload = payload; // 2.2 如果 setState 有回調,順便把回調賦值給 update 對象的 callback 屬性 if (callback !== undefined && callback !== null) { update.callback = callback; } // 3. 將 update 對象關聯到 Fiber 節點的 updateQueue 屬性中 enqueueUpdate(fiber, update); // 4. 發起調度 var root = scheduleUpdateOnFiber(fiber, lane, eventTime); }
從上面源碼可以清晰知道,setState
調用之后做的4件事情
- 根據組件實例獲取其 Fiber 節點
- 創建
Update
對象 - 將
Update
對象關聯到 Fiber 節點的updateQueue
屬性中 - 發起調度
根據組件實例獲取其 Fiber 節點
其實就是拿組件實例中的 _reactInternals
屬性,這個就是當前組件所對應的 Fiber 節點
function get(key) { return key._reactInternals; }
題外話:react利用雙緩存機制來完成 Fiber 樹的構建和替換,也就是 current
和 workInProgress
兩棵樹,那 enqueueSetState
里面拿的是那棵樹下的 Fiber 節點呢?
答案是:current樹下的Fiber節點。具體的原理在下面update對象丟失問題再說明
創建update對象
function createUpdate(eventTime, lane) { var update = { eventTime: eventTime, lane: lane, tag: UpdateState, payload: null, callback: null, next: null }; return update; }
屬性的含義如下:
- eventTime:update對象創建的時間,用于
ensureRootIsScheduled
計算過期時間用 - lane:此次更新的優先級
- payload:setState的第一個參數
- callback:setState的第二個參數
- next:連接的下一個 update 對象
將Update對象關聯到Fiber節點的updateQueue屬性
這里執行的是 enqueueUpdate
函數,下面是我簡化過后的邏輯
function enqueueUpdate(fiber, update) { var updateQueue = fiber.updateQueue; var sharedQueue = updateQueue.shared; var pending = sharedQueue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; }
可以看到這里的邏輯主要是將 update 對象放到 fiber 對象的 updateQueue.shared.pending
屬性中, updateQueue.shared.pending
是一個環狀鏈表。
那為什么需要把它設計為一個環狀鏈表?我是這樣理解的
-
shared.pending
存放的是鏈表的最后一個節點,那么在環狀鏈表中,鏈表的最后一個節點的next指針,是指向環狀鏈表的頭部節點,這樣我們就能快速知道鏈表的首尾節點 - 當知道首尾節點后,就能很輕松的合并兩個鏈表。比如有兩條鏈表a、b,我們想要把 b append到 a 的后面,可以這樣做
const lastBPoint = bTail const firstBPoint = bTail.next lastBPoint.next = null aTail.next = firstBPoint aTail = lastBPoint
后面即使有c、d鏈表,同樣也可以用相同的辦法合并到a。react 在構建 updateQueue
鏈表上也用了類似的手法,新產生的 update
對象通過類似上面的操作合并到 updateQueue
鏈表,
發起調度
在 enqueueUpdate
末尾,執行了 scheduleUpdateOnFiber
函數,該方法最終會調用 ensureRootIsScheduled
函數來調度react的應用根節點。
當進入 performConcurrentWorkOnRoot
函數時,就代表進入了 reconcile
階段,也就是我們說的 render
階段。render
階段是一個自頂向下再自底向上的過程,從react的應用根節點開始一直向下遍歷,再從底部節點往上回歸,這就是render
階段的節點遍歷過程。
這里我們需要知道的是,在render
階段自頂向下遍歷的過程中,如果遇到組件類型的Fiber節點,我們會執行 processUpdateQueue
函數,這個函數主要負責的是組件更新時 state 的計算
processUpdateQueue做了什么
processUpdateQueue
函數主要做了三件事情
- 構造本輪更新的
updateQueue
,并緩存到 currentFiber 節點中 - 循環遍歷
updateQueue
,計算得到newState
,構造下輪更新的updateQueue
- 更新 workInProgress 節點中的
updateQueue
、memoizedState
屬性
這里的 updateQueue
并不指代源碼中 Fiber 節點的 updateQueue
,可以理解為從 firstBaseUpdate
到 lastBaseUpdate
的整條更新隊列。這里為了方便描述和理解,直接用 updateQueue
替代說明。
變量解釋
因為涉及的變量比較多,processUpdateQueue
函數的邏輯看起來并不怎么清晰,所以我先列出一些變量的解釋方便理解
- shared.pending:
enqueueSetState
產生的 update對象 環形鏈表
- first/lastBaseUpdate:-- 下面我會用 baseUpdate 代替
當前 Fiber 節點中 updateQueue
對象中的屬性,代表當前組件整個更新隊列鏈表的首尾節點
- first/lastPendingUpdate:下面我會用 pendingUpdate 代替
shared.pending
剪開后的產物,分別代表新產生的 update對象 鏈表的首尾節點,最終會合并到 currentFiber 和 workInProgress 兩棵樹的更新隊列尾部
- newFirst/LastBaseUpdate:下面我會用 newBaseUpdate 代替
newState計算過程會得到,只要存在低優先級的 update 對象,這兩個變量就會有值。這兩個變量會賦值給 workInProgress 的 baseUpdate
,作為下一輪更新 update對象 鏈表的首尾節點
- baseState:newState 計算過程依賴的初始 state
- memoizedState:當前組件實例的 state,
processUpdateQueue
末尾會將 newState 賦值給這個變量,
構造本輪更新的 updateQueue
上面我們說到 shared.pending
是enqueueSetState
產生的 update對象 環形鏈表,在這里我們需要剪斷這個環形列表取得其中的首尾節點,去組建我們的更新隊列。那如何剪斷呢?
shared.pending
是環形鏈表的尾部節點,它的下一個節點就是環形鏈表的頭部節點,參考上一小節我們提到的鏈表合并操作。
var lastPendingUpdate = shared.pending; var firstPendingUpdate = lastPendingUpdate.next; lastPendingUpdate.next = null;
這樣就能剪斷環形鏈表,拿到我們想要的新的 update 對象 —— pendingUpdate
。接著我們要拿著這個 pendingUpdate
做兩件事情:
- 將
pendingUpdate
合并到當前Fiber節點的更新隊列 - 將
pendingUpdate
合并到 currentFiber樹 中對應 Fiber節點 的更新隊列
為什么要做這兩件事情?
- 第一個是解決狀態連續性問題,當出現多個 setState 更新時,我們要確保當前 update對象 的更新是以前一個 update對象 計算出來的 state 為前提。所以我們需要構造一個更新隊列,新的 update對象 要合并到更新隊列的尾部,從而維護state計算的連續性
- 第二個是解決 update 對象丟失問題。在
shared.pending
被剪開之后,shared.pending
會被賦值為null,當有高優先級任務進來時,低優先級任務就會被打斷,也就意味著 workInProgress 樹會被還原,shared.pending
剪開之后得到的pendingUpdate
就會丟失。這時就需要將pendingUpdate
合并到 currentFiber樹 的更新隊列中
接下來可以大致看一下這一部分的源碼
var queue = workInProgress.updateQueue; var firstBaseUpdate = queue.firstBaseUpdate; var lastBaseUpdate = queue.lastBaseUpdate; // 1. 先拿到本次更新的 update對象 環形鏈表 var pendingQueue = queue.shared.pending; if (pendingQueue !== null) { // 2. 清空pending queue.shared.pending = null; var lastPendingUpdate = pendingQueue; var firstPendingUpdate = lastPendingUpdate.next; // 3. 剪開環形鏈表 lastPendingUpdate.next = null; // 4. 將 pendingupdate 合并到 baseUpdate if (lastBaseUpdate === null) { firstBaseUpdate = firstPendingUpdate; } else { lastBaseUpdate.next = firstPendingUpdate; } lastBaseUpdate = lastPendingUpdate; // 5. 將 pendingupdate 合并到 currentFiber樹的 baseUpdate var current = workInProgress.alternate; if (current !== null) { var currentQueue = current.updateQueue; var currentLastBaseUpdate = currentQueue.lastBaseUpdate; if (currentLastBaseUpdate !== lastBaseUpdate) { if (currentLastBaseUpdate === null) { currentQueue.firstBaseUpdate = firstPendingUpdate; } else { currentLastBaseUpdate.next = firstPendingUpdate; } currentQueue.lastBaseUpdate = lastPendingUpdate; } } }
源碼看起來很多,但本質上只做了一件事,從源碼中可以看出這部分主要就是把 shared.pending
剪開,拿到我們的 pendingUpdate
,再把 pendingUpdate
合并到本輪更新和 currentFiber 節點的 baseUpdate
中。
計算 newState
在這部分的源碼中,除了計算 newState
,還有另外一個重要工作是,構造下一輪更新用的 updateQueue
。
到這里可能會有疑問,為什么需要構造下輪更新的 updateQueue
,本輪更新我們把 shared.pending
里面的對象遍歷計算完,再把 state 更新,下輪更新進來再根據這個 state 計算不行好了嗎?
如果沒有高優先級任務打斷機制,確實是不需要在這里構造下輪更新的 updateQueue
,因為每輪更新我們只會依賴當前的 state 和 shared.pending
。
打斷機制下,低優先級任務重啟后的執行,需要依賴完整的更新隊列才能保證 state 的連續性和正確性。下面我舉個例子
state = { count: 0 } componentDidMount() { const button = this.buttonRef.current // 低優先級任務 setTimeout(() => this.setState({ count: 1 }), 1000) // 高優先級任務 setTimeout(() => button.click(), 1040) } handleButtonClick = () => { this.setState( prevState => { return { count: prevState.count + 2 } } ) }
我們期望能實現的效果是 0 -> 2 -> 3
,需求如下:
- 高優先級任務打斷低優先級任務之后,不以低優先級任務計算得到的baseState做計算
- 低優先級任務重啟后,不能覆蓋高優先級任務計算得到的值,且需要根據低優先級任務計算得到的newState,作為高優先級的baseState再去執行一次高優先級任務
知道了需求,我們可以大概列一下實現思路:
- 低優先級任務打斷后,高優先級任務執行之前,需要還原到低優先級任務執行之前的 workInPregress 節點,確保不受低優先級任務計算得到的 baseState 影響
- 需要維護一個更新對象隊列,按執行順序存儲 update 對象,確保低優先級重啟后,依然會執行高優先級任務
上面說的需求和實現思路在 react 的源碼中實現其實是非常簡單的,但要理解其中的含義可能需要費點功夫,下面可以看看我改動過后的源碼,可以直接從 do...while
開始看
function cloneUpdate(update) { return { eventTime: update.eventTime, lane: update.lane, tag: update.tag, payload: update.payload, callback: update.callback, next: null }; } if (firstBaseUpdate !== null) { var newState = queue.baseState; var newBaseState = null; var newFirstBaseUpdate = null; var newLastBaseUpdate = null; var update = firstBaseUpdate; // 遍歷 updateQueue do { var updateLane = update.lane; var updateEventTime = update.eventTime; // 校驗當前 update 對象夠不夠優先級 if (!isSubsetOfLanes(renderLanes, updateLane)) { // 優先級不夠,我們需要從當前 update 對象開始重新構造一個更新隊列 var clone = cloneUpdate(update) if (newLastBaseUpdate === null) { newFirstBaseUpdate = newLastBaseUpdate = clone; // 當前的 newState 就作為下輪更新的 baseState 使用 newBaseState = newState; } else { newLastBaseUpdate = newLastBaseUpdate.next = clone; } } else { // 優先級夠 if (newLastBaseUpdate !== null) { // newLastBaseUpdate 不為空,就代表存在優先級不夠的 update 對象 var _clone = cloneUpdate(update) // 為保證狀態連續性,即使當前 update 對象優先級足夠,也要被放到 updateQueue 中 newLastBaseUpdate = newLastBaseUpdate.next = _clone; } // 計算newState newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance); } update = update.next; } while (update);
邏輯如下:
優先級不夠
- 重新構造更新隊列
newBaseUpdate
,留到低優先級任務重啟遍歷 - 記錄當前
newState
,留到低優先級任務重啟作為 baseState 計算
優先級足夠
- 看看
newBaseUpdate
有沒有東西,有東西就把當前 update 對象也合并進去 - 計算
newState
這里 newState
的計算邏輯很簡單
- payload是值。用對象包裹合并到 prevState 即可
- payload是函數。傳入 prevState 計算,將函數返回值也合并到 prevState 即可
更新 workInProgress 節點
更新 workInProgress 節點屬性的邏輯不多,主要就是把 newBaseState、newBaseUpate
賦值給 workInProgress 節點,作為下一輪更新的 baseState
和更新隊列使用
if (newLastBaseUpdate === null) { newBaseState = newState; } queue.baseState = newBaseState; queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; workInProgress.memoizedState = newState;
- 如果
newLastBaseUpdate
為空,代表所有 update 對象為空,本輪更新計算得到的newState
可以完全作為下輪更新的baseState
使用。否則只能用出現首個不夠優先級的 update 對象時緩存下來的newState
作為下輪更新的baseState
- 更新
baseUpdate
,當所有 update 對象優先級足夠,baseUpdate
的值一般為空。只有存在優先級不夠的 update 對象時,才會有值 - 將
newState
賦值給memoizedState
,memoizedState
代表當前組件的所有 state
總結
看到上面的原理解析是不是很復雜,我們可以忽略所有的實現細節,回歸現象本質,state計算就是遍歷 update對象 鏈表根據 payload 得到新的state。在此前提下,因為優先級機制,打斷之后會還原 workInProgress
節點,從而會引起 update對象 丟失問題 和 state計算連續性問題。解決這兩個問題才是我們上面說的復雜的實現細節
update對象丟失問題
為什么會丟失
我們知道高優先級任務進來會打斷低優先級任務的執行,打斷之后會將當前的 workInProgress
節點還原為開始的狀態,也就是可以理解為會將 workInProgress
樹還原為當前頁面所渲染的 currentFiber
節點。當 workInProgress
節點還原之后,我們本來存在 workInProgress
中的 updateQueue
屬性也會被重置,那就意味著低優先級的 update 對象會丟失。
上面說到的,setState產生的新 update對象 是會放在 currentFiber
節點上也是這個原因,如果 setState 產生的新 update對象 放到 workInProgress
上,只要 workInProgress
被還原,這些 update對象 就會丟失
如何解決
我們在 processUpdateQueue
函數的開始階段,將新產生的 update 對象,也就是 shared.pending
中的值,合并到 currentFiber( workInProgress.alternate )
節點的 firstBaseUpdate
和 lastBaseUpdate
。具體規則如下
-
currentFiber
節點不存在lastBaseUpdate
,將新的 update 對象賦值給currentFiber
節點的firstBaseUpdate
和lastBaseUpdate
屬性 -
currentFiber
節點存在lastBaseUpdate
,將新的 update 對象拼接到currentFiber
節點的lastBaseUpdate
節點后面,也就是說新的 update 對象會成為currentFiber
節點新的lastBaseUpdat
節點
還原 workInProgress
節點執行的函數是 prepareFreshStack
,里面會用 currentFiber
節點的屬性覆蓋 workInProgress
節點,從而實現還原功能。所以就算 workInProgress
節點被重置,我們只要把 update對象 合并到 currentFiber
節點上,還原的時候依然會存在于新的 workInProgress
節點
state計算的連續性
問題現象
我們上面說到,低優先級任務重啟,不能覆蓋高優先級任務計算得到的值,且需要根據低優先級任務計算得到的newState,作為高優先級的baseState再去執行一次高優先級任務。什么意思呢這是?
state = { count: 0 } componentDidMount() { const button = this.buttonRef.current // 低優先級任務 - AUpate setTimeout(() => this.setState({ count: 1 }), 1000) // 高優先級任務 - BUpdate setTimeout(() => button.click(), 1040) } handleButtonClick = () => { this.setState( prevState => { return { count: prevState.count + 2 } } ) }
上面代碼所產生的update對象如下
AUpate = { lane: 低, payload: 1 } BUpdate = { lane: 高, payload: state => ({ count: state.count + 2 }) }
- 先執行
AUpdate
任務 -
AUpdate
的優先級比BUpdate
的低,BUpdate
會打斷AUpdate
的執行。 - 那么
BUpdate
執行完,count的值為2 問題來了 -
BUpdate
是后進來的,AUpdate
不能覆蓋掉BUpdate
的結果 -
AUpdate
執行的結果 count 會變成 1,那么BUpdate
的結果需要在此基礎上計算,也就是要得到3
這也就決定了我們要用隊列的形式去存儲所有 update對象。update對象的存儲順序決定了state計算的前后依賴性,從而保證狀態的連續性和準確性
明確很重要的一點,優先級高低只會影響某個 update對象 是否會提前執行,不會影響最終的 state 結果。最終的 state 結果還是由更新隊列中 update對象 的順序決定的
如何解決
我們看到 processUpdateQueue
中有兩部分都是在構造更新隊列的
- 一部分是位于函數開頭的,將 update對象 合并到
currentFiber
節點 - 一部分是位于函數末尾的,將
newBaseUpdate
賦值給workInProgress
節點 這兩部分雙劍合璧就完美解決我們的需求,currentFiber
是作用于本輪更新,workInProgress
則作用于下一輪更新,因為雙緩存機制的存在,在 commit階段 結尾,react 應用根節點的 current 指針就會指向workInProgress
節點,workInProgress
節點在下一輪更新就會變成currentFiber
節點。
這樣無論是什么優先級,只要按順序構造出更新隊列,我就能計算出正確的newState,同時利用隊列的性質,保證 update對象 間 state計算 的連續性
原文鏈接:https://juejin.cn/post/7163102775578984456
相關推薦
- 2023-03-03 Linux命令之mkdir,cat,touch,vi/vim的詳解_Linux
- 2022-11-17 詳解C/C++實現各種字符轉換方法合集_C 語言
- 2022-07-21 提高新手寫代碼效率的Emmet插件怎么使用
- 2022-08-15 當添加一個鍵值對元素時,HashMap發生了什么?
- 2022-05-02 C語言如何實現一些算法或者函數你知道嗎_C 語言
- 2022-11-13 Python反射機制案例超詳細講解_python
- 2023-06-20 React?DOM-diff?節點源碼解析_React
- 2022-01-08 關于git操作warning: adding embedded git repository: pp
- 最近更新
-
- 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同步修改后的遠程分支