<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
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件事情
Update
物件Update
物件關聯到 Fiber 節點的 updateQueue
屬性中其實就是拿元件範例中的 _reactInternals
屬性,這個就是當前元件所對應的 Fiber 節點
function get(key) { return key._reactInternals; }
題外話:react利用雙快取機制來完成 Fiber 樹的構建和替換,也就是 current
和 workInProgress
兩棵樹,那 enqueueSetState
裡面拿的是那棵樹下的 Fiber 節點呢?
答案是:current樹下的Fiber節點。具體的原理在下面update物件丟失問題再說明
function createUpdate(eventTime, lane) { var update = { eventTime: eventTime, lane: lane, tag: UpdateState, payload: null, callback: null, next: null }; return update; }
屬性的含義如下:
ensureRootIsScheduled
計算過期時間用這裡執行的是 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指標,是指向環狀連結串列的頭部節點,這樣我們就能快速知道連結串列的首尾節點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
函數主要做了三件事情
updateQueue
,並快取到 currentFiber 節點中updateQueue
,計算得到 newState
,構造下輪更新的 updateQueue
updateQueue
、memoizedState
屬性這裡的 updateQueue
並不指代原始碼中 Fiber 節點的 updateQueue
,可以理解為從 firstBaseUpdate
到 lastBaseUpdate
的整條更新佇列。這裡為了方便描述和理解,直接用 updateQueue
替代說明。
因為涉及的變數比較多,processUpdateQueue
函數的邏輯看起來並不怎麼清晰,所以我先列出一些變數的解釋方便理解
enqueueSetState
產生的 update物件 環形連結串列
當前 Fiber 節點中 updateQueue
物件中的屬性,代表當前元件整個更新佇列連結串列的首尾節點
shared.pending
剪開後的產物,分別代表新產生的 update物件 連結串列的首尾節點,最終會合併到 currentFiber 和 workInProgress 兩棵樹的更新佇列尾部
newState計算過程會得到,只要存在低優先順序的 update 物件,這兩個變數就會有值。這兩個變數會賦值給 workInProgress 的 baseUpdate
,作為下一輪更新 update物件 連結串列的首尾節點
processUpdateQueue
末尾會將 newState 賦值給這個變數,上面我們說到 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節點 的更新佇列為什麼要做這兩件事情?
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
,需求如下:
知道了需求,我們可以大概列一下實現思路:
上面說的需求和實現思路在 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
的計算邏輯很簡單
更新 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計算連續性問題。解決這兩個問題才是我們上面說的複雜的實現細節
我們知道高優先順序任務進來會打斷低優先順序任務的執行,打斷之後會將當前的 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
節點
我們上面說到,低優先順序任務重啟,不能覆蓋高優先順序任務計算得到的值,且需要根據低優先順序任務計算得到的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
中有兩部分都是在構造更新佇列的
currentFiber
節點newBaseUpdate
賦值給 workInProgress
節點 這兩部分雙劍合璧就完美解決我們的需求,currentFiber
是作用於本輪更新,workInProgress
則作用於下一輪更新,因為雙快取機制的存在,在 commit階段 結尾,react 應用根節點的 current 指標就會指向 workInProgress
節點,workInProgress
節點在下一輪更新就會變成 currentFiber
節點。這樣無論是什麼優先順序,只要按順序構造出更新佇列,我就能計算出正確的newState,同時利用佇列的性質,保證 update物件 間 state計算 的連續性
以上就是React原始碼state計算流程和優先順序範例解析的詳細內容,更多關於React state計算流程優先順序的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45