日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React狀態更新的優先級機制源碼解析_React

作者:goClient1992 ? 更新時間: 2022-12-08 編程語言

為什么需要優先級

優先級機制最終目的是為了實現高優先級任務優先執行,低優先級任務延后執行

實現這一目的的本質就是在低優先級任務執行時,有更高優先級任務進來的話,可以打斷低優先級任務的執行。

同步模式下的react運行時

我們知道在同步模式下,從 setState 到 虛擬DOM遍歷,再到真實DOM更新,整個過程都是同步執行且無法被中斷的,這樣可能就會出現一個問題 —— 用戶事件觸發的更新被阻塞。

什么是用戶事件觸發的更新被阻塞?如果 React 正在進行更新任務,此時用戶觸發了交互事件,且在事件回調中執行了 setState,在同步模式下,這個更新任務需要 等待 當前正在更新的任務完成之后,才會被執行。假如當前 React 正在進行的更新任務耗時比較久,用戶事件觸發的更新任務不能及時被執行,造成下個更新任務被阻塞,從而形成了卡頓。

這時候,我們就希望能夠及時響應用戶觸發的事件,優先執行用戶事件觸發的更新任務,也就是我們說的異步模式

我們可以比較一下,同步模式下和異步模式(優先級機制)下更新任務執行的差異

import React from "react";
import "./styles.css";
export default class extends React.Component {
  constructor() {
    super();
    this.state = {
      list: new Array(10000).fill(1),
    };
    this.domRef = null;
  }
  componentDidMount() {
    setTimeout(() => {
      console.log("setTimeout 準備更新", performance.now());
      this.setState(
        {
          list: new Array(10000).fill(Math.random() * 10000),
          updateLanes: 16
        },
        () => {
          console.log("setTimeout 更新完畢", performance.now());
        }
      );
    }, 100);
    setTimeout(() => {
      this.domRef.click();
    }, 150);
  }
  render() {
    const { list } = this.state;
    return (
      <div
        ref={(v) => (this.domRef = v)}        className="App"        onClick={() => {          console.log("click 準備更新", performance.now());          this.setState(            { list: new Array(10000).fill(2), updateLanes: 1 },            () => {              console.log("click 更新完畢", performance.now());            }          );        }}      >        {list.map((i, index) => (          <h2 key={i + +index}>Hello {i}</h2>
        ))}      </div>
    );
  }
}

click事件 觸發的更新,會比 setTimeout 觸發的更新更優先執行,做到了及時響應用戶事件,打斷 setTimeout 更新任務(低優先級任務)的執行。

如何運用優先級機制優化react運行時

為了解決同步模式渲染下的缺陷,我們希望能夠對 react 做出下面這些優化

  • 確定不同場景下所觸發更新的優先級,以便我們可以決定優先執行哪些任務
  • 若有更高優先級的任務進來,我們需要打斷當前進行的任務,然后執行這個高優先級任務
  • 確保低優先級任務不會被一直打斷,在一定時間后能夠被升級為最高優先級的任務

確定不同場景下的調度優先級

看過 react 源碼的小伙伴可能都會有一個疑惑,為什么源碼里面有那么多優先級相關的單詞??怎么區分他們呢?

其實在 react 中主要分為兩類優先級,scheduler 優先級和 lane 優先級,lane優先級下面又派生出 event 優先級

  • lane 優先級:主要用于任務調度前,對當前正在進行的任務和被調度任務做一個優先級校驗,判斷是否需要打斷當前正在進行的任務
  • event 優先級:本質上也是lane優先級,lane優先級是通用的,event優先級更多是結合瀏覽器原生事件,對lane優先級做了分類和映射
  • scheduler 優先級:主要用在時間分片中任務過期時間的計算

lane優先級

可以用賽道的概念去理解lane優先級,lane優先級有31個,我們可以用31位的二進制值去表示,值的每一位代表一條賽道對應一個lane優先級,賽道位置越靠前,優先級越高

優先級 十進制值 二進制值 賽道位置
NoLane 0 0000000000000000000000000000000 0
SyncLane 1 0000000000000000000000000000001 0
InputContinuousHydrationLane 2 0000000000000000000000000000010 1
InputContinuousLane 4 0000000000000000000000000000100 2
DefaultHydrationLane 8 0000000000000000000000000001000 3
DefaultLane 16 0000000000000000000000000010000 4
TransitionHydrationLane 32 0000000000000000000000000100000 5
TransitionLane1 64 0000000000000000000000001000000 6
TransitionLane2 128 0000000000000000000000010000000 7
TransitionLane3 256 0000000000000000000000100000000 8
TransitionLane4 512 0000000000000000000001000000000 9
TransitionLane5 1024 0000000000000000000010000000000 10
TransitionLane 2048 0000000000000000000100000000000 11
TransitionLane7 4096 0000000000000000001000000000000 12
TransitionLane8 8192 0000000000000000010000000000000 13
TransitionLane9 16384 0000000000000000100000000000000 14
TransitionLane10 32768 0000000000000001000000000000000 15
TransitionLane11 65536 0000000000000010000000000000000 16
TransitionLane12 131072 0000000000000100000000000000000 17
TransitionLane13 262144 0000000000001000000000000000000 18
TransitionLane14 524288 0000000000010000000000000000000 19
TransitionLane15 1048576 0000000000100000000000000000000 20
TransitionLane16 2097152 0000000001000000000000000000000 21
RetryLane1 4194304 0000000010000000000000000000000 22
RetryLane2 8388608 0000000100000000000000000000000 23
RetryLane3 16777216 0000001000000000000000000000000 24
RetryLane4 33554432 0000010000000000000000000000000 25
RetryLane5 67108864 0000100000000000000000000000000 26
SelectiveHydrationLane 134217728 0001000000000000000000000000000 27
IdleHydrationLane 268435456 0010000000000000000000000000000 28
IdleLane 536870912 0100000000000000000000000000000 29
OffscreenLane 1073741824 1000000000000000000000000000000 30

event優先級

EventPriority ? Lane 數值
DiscreteEventPriority 離散事件。click、keydown、focusin等,事件的觸發不是連續,可以做到快速響應 SyncLane 1
ContinuousEventPriority 連續事件。drag、scroll、mouseover等,事件的是連續觸發的,快速響應可能會阻塞渲染,優先級較離散事件低 InputContinuousLane 4
DefaultEventPriority 默認的事件優先級 DefaultLane 16
IdleEventPriority 空閑的優先級 IdleLane 536870912

scheduler優先級

SchedulerPriority EventPriority 大于>17.0.2 小于>17.0.2
ImmediatePriority DiscreteEventPriority 1 99
UserblockingPriority Userblocking 2 98
NormalPriority DefaultEventPriority 3 97
LowPriority DefaultEventPriority 4 96
IdlePriority IdleEventPriority 5 95
NoPriority ? 0 90

優先級間的轉換

lane優先級 轉 event優先級(參考 lanesToEventPriority 函數)

  • 轉換規則:以區間的形式根據傳入的lane返回對應的 event 優先級。比如傳入的優先級不大于 Discrete 優先級,就返回 Discrete 優先級,以此類推

event優先級 轉 scheduler優先級(參考 ensureRootIsScheduled 函數)

  • 轉換規則:可以參考上面scheduler優先級表

event優先級 轉 lane優先級(參考 getEventPriority 函數)

  • 轉換規則:對于非離散、連續的事件,會根據一定規則作轉換,具體課參考上面 event 優先級表,

優先級機制如何設計

說到優先級機制,我們可能馬上能聯想到的是優先級隊列,其最突出的特性是最高優先級先出react 的優先級機制跟優先級隊列類似,不過其利用了賽道的概念,配合位與運算豐富了隊列的功能,比起優先級隊列,讀寫速度更快,更加容易理解

設計思路

  • 合并賽道:維護一個隊列,可以存儲被占用的賽道
  • 釋放賽道:根據優先級釋放對應被占用賽道
  • 找出最高優先級賽道:獲取隊列中最高優先級賽道
  • 快速定位賽道索引:根據優先級獲取賽道在隊列中所在的位置
  • 判斷賽道是否被占用:根據傳入優先級判斷該優先級所在賽道是否被占用

合并賽道

場景

  • 比如當前正在調度的任務優先級是DefaultLane,用戶點擊觸發更新,有一個高優先級的任務SyncLane產生,需要存儲這個任務所占用的賽道

運算過程

  • 運算方式:位或運算 - a | b
  • 運算結果:DefaultLane和SyncLane分別占用了第1條和第5條賽道

DefaultLane優先級為16,SyncLane優先級為1

16 | 1 = 17

17的二進制值為10001
16的二進制值為10000,1的二進制值為00001

釋放賽道

場景

  • SyncLane 任務執行完,需要釋放占用的賽道

運算過程

  • 運算方式:位與+位非 - a & ~b
  • 運算結果:SyncLane賽道被釋放,只剩下DefaultLane賽道

17 & ~1 = 16
17的二進制值為10001

為什么用位非?
~1 = -2
2 的二進制是00010,-2的話符號位取反變為10010
10001和10010進行位與運算得到10000,也就是十進制的16

找出最高優先級賽道

場景

  • 當前有 DefaultLane 和 SyncLane 兩個優先級的任務占用賽道,在進入 ensureRootIsScheduled 方法后,我需要先調度優先級最高的任務,所以需要找出當前優先級最高的賽道

運算過程

  • 運算方式:位與+符號位取反 - a & -b
  • 運算結果:找到了最高優先級的任務SyncLane,SyncLane任務為同步任務,Scheduler將以同步優先級調度當前應用根節點

17 & -17 = 1

17的二進制值為10001
-17的二進制值為00001
10001和00001進行位與運算得到1,也就是SyncLane

快速定位賽道索引

場景

  • 饑餓任務喚醒:在發起調度前,我們需要對隊列中的所有賽道進行一個判斷,判斷該賽道的任務是否過期,如果過期,就優先執行該過期任務。為此,需要維護一個長度為31的數組,數組的每個元素的下標索引與31個優先級賽道一一對應,數組中存儲的是任務的過期時間,在判斷時,我們希望能根據優先級快速找到該優先級在數組中對應的位置。

運算過程

  • 運算方式:Math.clz32
  • 運算結果:找到了DefaultLane的索引位置為4,那就可以釋放應用根節點上的eventTimes、expirationTimes,將其所在位置的值賦值為-1,然后執行對應的過期任務

// 找出 DefaultLane 賽道索引
31 - Math.clz32(16) = 4

16的二進制值為10000
索引4對應的就是第五個賽道

Math.clz32是用來干什么的?

  • 獲取一個十進制數字對應二進制值中開頭0的個數。
  • 所以用31減去 Math.clz32 的值就能得到該賽道的索引

判斷賽道是否被占用

異步模式下會存在高優先級任務插隊的情況,此情況下 state 的計算方式會跟同步模式下**有些不同。

場景

我們 setState 之后并不是馬上就會更新 state,而是會根據 setState 的內容生成一個 Update 對象,這個對象包含了更新內容、更新優先級等屬性。

更新 state 這個動作是在 processUpdateQueue 函數里進行的,函數里面會判斷 Update 對象的優先級所在賽道是否被占用,來決定是否在此輪任務中計算這個 Update 對象的 state

  • 如果被占用,代表 Update 對象優先級和當前正在進行的任務相等,可以根據 Update 對象計算 state 并更新到 Fiber 節點的 memoizedState 屬性上
  • 如果未被占用,代表當前正在進行的任務優先級比這個 Update 對象優先級高,相應的這個低優先級的 Update 對象將暫不被計算state,留到下一輪低優先級任務被重啟時再進行計算

運算過程

  • 運算方式:位與 (renderLanes & updateLanes) == updateLanes
  • 運算結果:0代表當前調度優先級高于某個Update對象優先級

運算公式
(1 & 16) == 16

1的二進制值為00001
16的二進制值為10000?
00001和10000進行位與運算得到0

如何將優先級機制融入React運行時

生成一個更新任務

生成任務的流程其實非常簡單,入口就在我們常用的 setState 函數,先上圖

setState 函數內部執行的就是 enqueueUpdate 函數,而 enqueueUpdate 函數的工作主要分為4步:

  • 獲取本次更新的優先級。
  • 創建 Update 對象
  • 將本次更新優先級關聯到當前Fiber節點、父級節點和應用根節點
  • 發起 ensureRootIsScheduled 調度。

步驟一:獲取本次更新的優先級

步驟一的工作是調用 requestUpdateLane 函數拿到此次更新任務的優先級

如果當前為非 concurrent 模式

  • 當前不在 render 階段。返回 syncLane
  • 當前正在 render 階段。返回 workInProgressRootRenderLanes 中最高的優先級(這里就用到上面的優先級運算機制,找出最高優先級賽道

如果當前為 concurrent 模式

  • 需要執行延遲任務的話,比如 SuspenduseTransitionuseDefferedValue 等特性。在 transition 類型的優先級中尋找空閑的賽道。transition類型的賽道有 16 條,從第 1 條到第 16 條,當到達第 16 條賽道后,下一次 transition 類型的任務會回到第 1 條賽道,如此往復。
  • 執行 getCurrentUpdatePriority 函數。獲取當前更新優先級。如果不為 NoLane 就返回
  • 執行 getCurrentEventPriority 函數。返回當前的事件優先級。如果沒有事件產生,返回 DefaultEventPriority

總的來說,requestUpdateLane 函數的優先級選取判斷順序如下:

SyncLane  >>  TransitionLane  >>  UpdateLane  >>  EventLane

估計有很多小伙伴都會很困惑一個問題,為什么會有這么多獲取優先級的函數,這里我整理了一下其他函數的職責

步驟二:創建 Update 對象

這里的代碼量不多,其實就是將 setState 的參數用一個對象封裝起來,留給 render 階段用

function createUpdate(eventTime, lane) {
  var update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  };
  return update;
}

步驟三:關聯優先級

在這里先解釋兩個概念,一個是 HostRoot,一個是 FiberRootNode

  • HostRoot:就是 ReactDOM.render 的第一個參數,組件樹的根節點。HostRoot可能會存在多個,因為 ReactDOM.render 可以多次調用
  • FiberRootNode:react 的應用根節點,每個頁面只有一個 react 的應用根節點。可以從 HostRoot 節點的 stateNode 屬性訪問

這里關聯優先級主要執行了兩個函數

markUpdateLaneFromFiberToRoot。該函數主要做了兩個事情

  • 將優先級合并到當前 Fiber 節點的 lanes 屬性中
  • 將優先級合并到父級節點的 childLanes 屬性中(告訴父節點他的子節點有多少條賽道要跑) 但因為函數傳入的 Fiber 節點是 HostRoot,也就是 ReactDOM.render 的根節點,也就是說沒有父節點了,所以第二件事情沒有做

markRootUpdated。該函數也是主要做了兩個事情

  • 將待調度任務優先級合并到當前 react 應用根節點上
  • 計算當前任務優先級賽道占用的開始時間(eventTime)

由此可見,react 的優先級機制并不獨立運行在每一個組件節點里面,而是依賴一個全局的 react 應用根節點去控制下面多個組件樹的任務調度

優先級關聯到這些Fiber節點有什么用?

先說說他們的區別

  • lanes:只存在非 react 應用根節點上,記錄當前 Fiber 節點的 lane 優先級
  • childLanes:只存在非 react 應用根節點上,記錄當前 Fiber 節點下的所有子 Fiber 節點的 lane 優先級
  • pendingLanes:只存在 react 應用根節點上,記錄的是所有 HostRoot 的 lane 優先級

具體應用場景

  • 釋放賽道。上面說的優先級運算機制提到了任務執行完畢會釋放賽道,具體來說是在 commit 階段結束之后釋放被占用的優先級,也就是 markRootFinished 函數。
  • 判斷賽道是否被占用。在 render 階段的 beginWork 流程里面,會有很多判斷 childLanes 是否被占用的判斷

步驟四:發起調度

調度里面最關鍵的一步,就是 ensureRootIsScheduled 函數的調用,該函數的邏輯就是由下面兩大部分構成,高優先級任務打斷低優先級任務饑餓任務問題

高優先級任務打斷低優先級任務

該部分流程可以分為三部曲

  • cancelCallback
  • pop(taskQueue)
  • 低優先級任務重啟

cancelCallback

var existingCallbackNode = root.callbackNode;
var existingCallbackPriority = root.callbackPriority;
var newCallbackPriority = getHighestPriorityLane(nextLanes);
if (existingCallbackPriority === newCallbackPriority) {
    ...
    return;
}
if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
}
newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

上面是 ensureRootIsScheduled 函數的一些代碼片段,先對變量做解釋

existingCallbackNode:當前 render 階段正在進行的任務

existingCallbackPriority:當前 render 階段正在進行的任務優先級

newCallbackPriority:此次調度優先級

這里會判斷 existingCallbackPrioritynewCallbackPriority 兩個優先級是否相等,如果相等,此次更新合并到當前正在進行的任務中。如果不相等,代表此次更新任務的優先級更高,需要打斷當前正在進行的任務

如何打斷任務?

  • 關鍵函數 cancelCallback(existingCallbackNode)cancelCallback 函數就是將 root.callbackNode 賦值為null
  • performConcurrentWorkOnRoot 函數會先把 root.callbackNode 緩存起來,在函數末尾會再判斷 root.callbackNode 和開始緩存起來的值是否一樣,如果不一樣,就代表 root.callbackNode 被賦值為null了,有更高優先級任務進來。
  • 此時 performConcurrentWorkOnRoot 返回值為null

下面是 performConcurrentWorkOnRoot 代碼片段

...
var originalCallbackNode = root.callbackNode;
...
// 函數末尾
if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
}
return null;

由上面 ensureRootIsScheduled 的代碼片段可以知道,performConcurrentWorkOnRoot 函數是被 scheduleCallback 函數調度的,具體返回后的邏輯需要到 Scheduler 模塊去找

pop(taskQueue)

var callback = currentTask.callback;
if (typeof callback === 'function') {
  ...
} else {
  pop(taskQueue);
}

上面是 Scheduler 模塊里面 workLoop 函數的代碼片段,currentTask.callback 就是 scheduleCallback 的第二個參數,也就是performConcurrentWorkOnRoot 函數

承接上個主題,如果 performConcurrentWorkOnRoot 函數返回了null,workLoop 內部就會執行 pop(taskQueue),將當前的任務從 taskQueue 中彈出。

低優先級任務重啟

上一步中說道一個低優先級任務從 taskQueue 中被彈出。那高優先級任務執行完畢之后,如何重啟回之前的低優先級任務呢?

關鍵是在 commitRootImpl 函數

var remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes);
...
ensureRootIsScheduled(root, now());

markRootFinished 函數剛剛上面說了是釋放已完成任務所占用的賽道,那也就是說未完成任務依然會占用其賽道,所以我們可以重新調用 ensureRootIsScheduled 發起一次新的調度,去重啟低優先級任務的執行。我們可以看下重啟部分的判斷

var nextLanes = getNextLanes(
    root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
// 如果 nextLanes 為 NoLanes,就證明所有任務都執行完畢了
if (nextLanes === NoLanes) {
    ...
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    // 只要 nextLanes 為 NoLanes,就可以結束調度了
    return;
}
// 如果 nextLanes 不為 NoLanes,就代表還有任務未執行完,也就是那些被打斷的低優先級任務
...

饑餓任務問題

上面說到,在高優先級任務執行完畢之后,低優先級任務就會被重啟,但假設如果持續有高優先級任務持續進來,我的低優先級任務豈不是沒有重啟之日?

所以 react 為了處理解決饑餓任務問題,react 在 ensureRootIsScheduled 函數開始的時候做了以下處理:(參考markStarvedLanesAsExpired函數)

var lanes = pendingLanes;
while (lanes > 0) {
    var index = pickArbitraryLaneIndex(lanes);
    var lane = 1 << index;
    var expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {
      if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      root.expiredLanes |= lane;
    }
    lanes &= ~lane;
}
  • 遍歷31條賽道,判斷每條賽道的過期時間是否為 NoTimestamp,如果是,且該賽道存在待執行的任務,則為該賽道初始化過期時間
  • 如果該賽道已存在過期時間,且過期時間已經小于當前時間,則代表任務已過期,需要將當前優先級合并到 expiredLanes,這樣在下一輪 render 階段就會以同步優先級調度當前 HostRoot

可以參考 render 階段執行的函數 performConcurrentWorkOnRoot 中的代碼片段

var exitStatus = shouldTimeSlice(root, lanes) && ( !didTimeout) ? 
                    renderRootConcurrent(root, lanes) : 
                    renderRootSync(root, lanes);

可以看到只要 shouldTimeSlice 只要返回 false,就會執行 renderRootSync,也就是以同步優先級進入 render 階段。而 shouldTimeSlice 的邏輯也就是剛剛的 expiredLanes 屬性相關

function shouldTimeSlice(root, lanes) {
  // 如果 expiredLanes 里面有東西,代表有饑餓任務
  if ((lanes & root.expiredLanes) !== NoLanes) {
    return false;
  }
  var SyncDefaultLanes = InputContinuousHydrationLane | 
                          InputContinuousLane | 
                          DefaultHydrationLane | 
                          DefaultLane;
  return (lanes & SyncDefaultLanes) === NoLanes;
}

總結

react 的優先級機制在源碼中并不是一個獨立的,解耦的模塊,而是涉及到了react整體運行的方方面面,最后回歸整理下優先級機制在源碼中的使用,讓大家對優先級機制有一個更加整體的認知。

  • 時間分片。涉及到任務打斷、根據優先級計算分片時長
  • setState 生成 Update 對象。每個 Update 對象里面都有一個 lane 屬性,代表此次更新的優先級
  • 高優先級任務打斷低優先級任務。每一次調度都會對正在進行任務和當前任務最高優先級做比較,如果不相等,就代表有高優先級任務進來,需要打斷當前正在的任務。
  • 低優先級任務重啟。協調 (reconcile) 的下一個階段是渲染 (renderer),也就是我們說的 commit 階段,在此階段末尾,會調用 ensureRootIsScheduled 發起一次新的調度,執行尚未完成的低優先級任務。
  • 饑餓任務喚醒。每次調度的開始,都會先檢查下有沒有過期任務,如果有的話,下一次就會以同步優先級進行 render 任務(reconcile),同步優先級就是最高的優先級,不會被打斷

原文鏈接:https://juejin.cn/post/7163447394493792264

欄目分類
最近更新