扒一扒React計算狀態的原理

nero發表於2021-01-19

點選進入React原始碼除錯倉庫。

概述

一旦使用者的互動產生了更新,那麼就會產生一個update物件去承載新的狀態。多個update會連線成一個環裝連結串列:updateQueue,掛載fiber上,
然後在該fiber的beginWork階段會迴圈該updateQueue,依次處理其中的update,這是處理更新的大致過程,也就是計算元件新狀態的本質。在React中,類元件與根元件使用一類update物件,函式元件則使用另一類update物件,但是都遵循一套類似的處理機制。暫且先以類元件的update物件為主進行講解。

相關概念

更新是如何產生的呢?在類元件中,可以通過呼叫setState產生一個更新:

this.setState({val: 6});

而setState實際上會呼叫enqueueSetState,生成一個update物件,並呼叫enqueueUpdate將它放入updateQueue。

const classComponentUpdater = {
  enqueueSetState(inst, payload, callback) {
   ...
   // 依據事件優先順序建立update的優先順序
   const lane = requestUpdateLane(fiber, suspenseConfig);
   const update = createUpdate(eventTime, lane, suspenseConfig);
   update.payload = payload;
   enqueueUpdate(fiber, update);
   // 開始排程
   scheduleUpdateOnFiber(fiber, lane, eventTime);
     ... 
 },
};

假設B節點產生了更新,那麼B節點的updateQueue最終會是是如下的形態:

         A 
        /
       /
      B ----- updateQueue.shared.pending = update————
     /                                       ^       |
    /                                        |_______|
   C -----> D
 

updateQueue.shared.pending中儲存著一個個的update。
下面我們講解以下update和updateQueue的結構。

update的結構

update物件作為更新的載體,必然要儲存更新的資訊

const update: Update<*> = {
 eventTime,
 lane,
 suspenseConfig,
 tag: UpdateState,
 payload: null,
 callback: null,
 next: null,
};
  • eventTime:update的產生時間,若該update一直因為優先順序不夠而得不到執行,那麼它會超時,會被立刻執行
  • lane:update的優先順序,即更新優先順序
  • suspenseConfig:任務掛起相關
  • tag:表示更新是哪種型別(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
  • payload:更新所攜帶的狀態。

    • 類元件中:有兩種可能,物件({}),和函式((prevState, nextProps):newState => {})
    • 根元件中:是React.element,即ReactDOM.render的第一個引數
  • callback:可理解為setState的回撥
  • next:指向下一個update的指標

updateQueue的結構

在元件上有可能產生多個update,所以對於fiber來說,需要一個連結串列來儲存這些update,這就是updateQueue,它的結構如下:

const queue: UpdateQueue<State> = {
     baseState: fiber.memoizedState,
     firstBaseUpdate: null,
     lastBaseUpdate: null,
     shared: {
         pending: null,
     },
    effects: null,
};

我們假設現在產生了一個更新,那麼以處理這個更新的時刻為基準,來看一下這些欄位的含義:

  • baseState:前一次更新計算得出的狀態,它是第一個被跳過的update之前的那些update計算得出的state。會以它為基礎計算本次的state
  • firstBaseUpdate:前一次更新時updateQueue中第一個被跳過的update物件
  • lastBaseUpdate:前一次更新中,updateQueue中以第一個被跳過的update為起點一直到的最後一個update擷取的佇列中的最後一個update。
  • shared.pending:儲存著本次更新的update佇列,是實際的updateQueue。shared的意思是current節點與workInProgress節點共享一條更新佇列。
  • effects:陣列。儲存update.callback !== null的Update

有幾點需要解釋一下:

  1. 關於產生多個update物件的場景,多次呼叫setState即可
this.setState({val: 2});
this.setState({val: 6});

產生的updateQueue結構如下:

可以看出它是個單向的環裝連結串列

 u1 ---> u2
 ^        |
 |________|
  1. 關於更新佇列為什麼是環狀。

結論是:這是因為方便定位到連結串列的第一個元素。updateQueue指向它的最後一個update,updateQueue.next指向它的第一個update。

試想一下,若不使用環狀連結串列,updateQueue指向最後一個元素,需要遍歷才能獲取連結串列首部。即使將updateQueue指向第一個元素,那麼新增update時仍然要遍歷到尾部才能將新增的接入連結串列。而環狀連結串列,只需記住尾部,無需遍歷操作就可以找到首部。理解概念是重中之重,下面再來看一下實現:

function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
   const updateQueue = fiber.updateQueue;
   if (updateQueue === null) {
       return;
   }
   const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; // ppending是真正的updateQueue,儲存update
   const pending = sharedQueue.pending;
   if (pending === null) { // 若連結串列中沒有元素,則建立單向環狀連結串列,next指向它自己
     update.next = update;
   } else {
     // 有元素,現有佇列(pending)指向的是連結串列的尾部update,
     // pending.next就是頭部update,新update會放到現有佇列的最後
     // 並首尾相連
     // 將新佇列的尾部(新插入的update)的next指向佇列的首部,實現
     // 首位相連
     update.next = pending.next; // 現有佇列的最後一個元素的next指向新來的update,實現把新update
     // 接到現有佇列上
     pending.next = update;
   } // 現有佇列的指標總是指向最後一個update,可以通過最後一個尋找出整條連結串列
   sharedQueue.pending = update;
}
  1. 關於firstBaseUpdate 和 lastBaseUpdate,它們兩個其實組成的也是一個連結串列:baseUpdate,以當前這次更新為基準,這個連結串列儲存的是上次updateQueue中第一個被跳過的低優先順序的update,到佇列中最後一個update之間的所有update。關於baseState,它是第一個被跳過的update之前的那些update計算的state。

這兩點稍微不好理解,下面用例子來說明:比如有如下的updateQueue:

A1 -> B1 -> C2 -> D1 - E2

字母表示update攜帶的狀態,數字表示update攜帶的優先順序。Lanes模型中,可理解為數越小,優先順序越高,所以 1 > 2

第一次以1的渲染優先順序處理佇列,遇到C2時,它的優先順序不為1,跳過。那麼直到這次處理完updateQueue時,此時的baseUpdate連結串列為

C2 -> D1 - E2

本次更新完成後,firstBaseUpdate 為 C2,lastBaseUpdate 為 E2,baseState為ABD

用firstBaseUpdate 和 lastBaseUpdate記錄下被跳過的update到最後一個update的所有update,用baseState記錄下被跳過的update之前那些update所計算出的狀態。這樣做的目的是保證最終updateQueue中所有優先順序的update全部處理完時候的結果與預期結果保持一致。也就是說,儘管A1 -> B1 -> C2 -> D1 - E2這個連結串列在第一次以優先順序為1去計算的結果為ABD(因為優先順序為2的都被跳過了),但最終的結果一定是ABCDE,因為這是佇列中的所有update物件被全部處理的結果,下邊來詳細剖析updateQueue的處理機制。

更新的處理機制

處理更新分為三個階段:準備階段、處理階段、完成階段。前兩個階段主要是處理updateQueue,最後一個階段來將新計算的state賦值到fiber上。

準備階段

整理updateQueue。由於優先順序的原因,會使得低優先順序更新被跳過等待下次執行,這個過程中,又有可能產生新的update。所以當處理某次更新的時候,有可能會有兩條update佇列:上次遺留的和本次新增的上次遺留的就是從firstBaseUpdate 到 lastBaseUpdate 之間的所有update;本次新增的就是新產生的那些的update。

準備階段階段主要是將兩條佇列合併起來,並且合併之後的佇列不再是環狀的,目的方便從頭到尾遍歷處理。另外,由於以上的操作都是處理的workInProgress節點的updateQueue,所以還需要在current節點也操作一遍,保持同步,目的在渲染被高優先順序的任務打斷後,再次以current節點為原型新建workInProgress節點時,不會丟失之前尚未處理的update。

處理階段

迴圈處理上一步整理好的更新佇列。這裡有兩個重點:

  • 本次更新是否處理update取決於它的優先順序(update.lane)和渲染優先順序(renderLanes)。
  • 本次更新的計算結果基於baseState。

優先順序不足

優先順序不足的update會被跳過,它除了跳過之外,還做了三件事:

  1. 將被跳過的update放到firstBaseUpdate 和 lastBaseUpdate組成的連結串列中,(就是baseUpdate),等待下次處理低優先順序更新的時候再處理。
  2. 記錄baseState,此時的baseState為該低優先順序update之前所有已被處理的更新的結果,並且只在第一次跳過時記錄,因為低優先順序任務重做時,要從第一個被跳過的更新開始處理。
  3. 將被跳過的update的優先順序記錄下來,更新過程即將結束後放到workInProgress.lanes中,這點是排程得以再次發起,進而重做低優先順序任務的關鍵。

關於第二點,ReactUpdateQueue.js檔案頭部的註釋做了解釋,為了便於理解,我再解釋一下。

第一次更新的baseState 是空字串,更新佇列如下,字母表示state,數字表示優先順序。優先順序是1 > 2的

 A1 - B1 - C2 - D1 - E2
 
 第一次的渲染優先順序(renderLanes)為 1,Updates是本次會被處理的佇列:
 Base state: ''
 Updates: [A1, B1, D1]      <- 第一個被跳過的update為C2,此時的baseUpdate佇列為[C2, D1, E2],
                               它之前所有被處理的update的結果是AB。此時記錄下baseState = 'AB'
                               注意!再次跳過低優先順序的update(E2)時,則不會記錄baseState
                               
 Result state: 'ABD'--------------------------------------------------------------------------------------------------
 
 
 第二次的渲染優先順序(renderLanes)為 2,Updates是本次會被處理的佇列:
 Base state: 'AB'           <- 再次發起排程時,取出上次更新遺留的baseUpdate佇列,基於baseState
                               計算結果。
                               
 Updates: [C2, D1, E2] Result state: 'ABCDE'

優先順序足夠

如果某個update優先順序足夠,主要是兩件事:

  • 判斷若baseUpdate佇列不為空(之前有被跳過的update),則將現在這個update放入baseUpdate佇列。
  • 處理更新,計算新狀態。

將優先順序足夠的update放入baseUpdate這一點可以和上邊低優先順序update入隊baseUpdate結合起來看。這實際上意味著一旦有update被跳過,就以它為起點,將後邊直到最後的update無論優先順序如何都擷取下來。再用上邊的例子來說明一下。

A1 - B2 - C1 - D2
B2被跳過,baseUpdate佇列為
B2 - C1 - D2

這樣做是為了保證最終全部更新完成的結果和使用者行為觸發的那些更新全部完成的預期結果保持一致。比如,A1和C1雖然在第一次被優先執行,展現的結果為AC,但這只是為了及時響應使用者互動產生的臨時結果,實際上C1的結果需要依賴B2計算結果,當第二次render時,依據B2的前序update的處理結果(baseState為A)開始處理B2 - C1 - D2佇列,最終的結果是ABCD。在提供的高優先順序任務插隊的例子中,可以證明這一點。

變化過程為 0 -> 2 -> 3,生命週期將state設定為1(任務A2),點選事件將state + 2(任務A1),正常情況下A2正常排程,但是未render完成,此時A1插隊,更新佇列A2 - A1,為了優先響應高優先順序的更新,跳過A2先計算A1,數字由0變為2,baseUpdate為A2 - A1,baseState為0。然後再重做低優先順序任務。處理baseUpdate A2 - A1,以baseState(0)為基礎進行計算,最後結果是3。

高優先順序插隊

完成階段

主要是做一些賦值和優先順序標記的工作。

  • 賦值updateQueue.baseState。若此次render沒有更新被跳過,那麼賦值為新計算的state,否則賦值為第一個被跳過的更新之前的update。
  • 賦值updateQueue 的 firstBaseUpdate 和 lastBaseUpdate,也就是如果本次有更新被跳過,則將被擷取的佇列賦值給updateQueue的baseUpdate連結串列。
  • 更新workInProgress節點的lanes。更新策略為如果沒有優先順序被跳過,則意味著本次將update都處理完了,lanes清空。否則將低優先順序update的優先順序放入lanes。之前說過,

此處是再發起一次排程重做低優先順序任務的關鍵。

  • 更新workInProgress節點上的memoizedState。

原始碼實現

上面基本把處理更新的所有過程敘述了一遍,現在讓我們看一下原始碼實現。這部分的程式碼在processUpdateQueue函式中,它裡面涉及到了大量的連結串列操作,程式碼比較多,
我們先來看一下它的結構,我標註出了那三個階段。

function processUpdateQueue<State>(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
   // 準備階段
   const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
   let firstBaseUpdate = queue.firstBaseUpdate;
   let lastBaseUpdate = queue.lastBaseUpdate;
   let pendingQueue = queue.shared.pending;
   if (pendingQueue !== null) { /* ... */ }
   
   if (firstBaseUpdate !== null) { // 處理階段
     do { ... } while (true);
     
     // 完成階段
     if (newLastBaseUpdate === null) {
        newBaseState = newState;
     }
     queue.baseState = ((newBaseState: any): State);
     queue.firstBaseUpdate = newFirstBaseUpdate;
     queue.lastBaseUpdate = newLastBaseUpdate;
     markSkippedUpdateLanes(newLanes);
     workInProgress.lanes = newLanes;
     workInProgress.memoizedState = newState;
   }
}

對於上面的概念與原始碼的主體結構瞭解之後,放出完整程式碼,但刪除了無關部分,我新增了註釋,對照著那三個過程來看會更有助於理解,否則單看連結串列操作還是有些複雜。

function processUpdateQueue<State>(
 workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void {
 // 準備階段----------------------------------------
 // 從workInProgress節點上取出updateQueue
 // 以下程式碼中的queue就是updateQueue
 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
 // 取出queue上的baseUpdate佇列(下面稱遺留的佇列),然後
 // 準備接入本次新產生的更新佇列(下面稱新佇列)
 let firstBaseUpdate = queue.firstBaseUpdate;
 let lastBaseUpdate = queue.lastBaseUpdate;
 // 取出新佇列
 let pendingQueue = queue.shared.pending;
 // 下面的操作,實際上就是將新佇列連線到上次遺留的佇列中。
 if (pendingQueue !== null) { queue.shared.pending = null;
 // 取到新佇列
 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next;
 // 將遺留的佇列最後一個元素指向null,實現斷開環狀連結串列
 // 然後在尾部接入新佇列
 lastPendingUpdate.next = null;
 if (lastBaseUpdate === null) {
   firstBaseUpdate = firstPendingUpdate;
 } else {
   // 將遺留的佇列中最後一個update的next指向新佇列第一個update
   // 完成接入
   lastBaseUpdate.next = firstPendingUpdate; } // 修改遺留佇列的尾部為新佇列的尾部
   lastBaseUpdate = lastPendingUpdate;
   // 用同樣的方式更新current上的firstBaseUpdate 和
   // lastBaseUpdate(baseUpdate佇列)。
   // 這樣做相當於將本次合併完成的佇列作為baseUpdate佇列備份到current節
   // 點上,因為如果本次的渲染被打斷,那麼下次再重新執行任務的時候,workInProgress節點複製
   // 自current節點,它上面的baseUpdate佇列會保有這次的update,保證update不丟失。
   const current = workInProgress.alternate;
   if (current !== null) {
   // This is always non-null on a ClassComponent or HostRoot
     const currentQueue:UpdateQueue<State> = (current.updateQueue: any);
     const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
     if (currentLastBaseUpdate !== lastBaseUpdate) {
       if (currentLastBaseUpdate === null) {
         currentQueue.firstBaseUpdate = firstPendingUpdate;
       } else {
         currentLastBaseUpdate.next = firstPendingUpdate;
       }
       currentQueue.lastBaseUpdate = lastPendingUpdate;
     }
   }
 }
 // 至此,新佇列已經合併到遺留佇列上,firstBaseUpdate作為
 // 這個新合併的佇列,會被迴圈處理
 // 處理階段-------------------------------------
 if (firstBaseUpdate !== null) { // 取到baseState
   let newState = queue.baseState;
   // 宣告newLanes,它會作為本輪更新處理完成的
   // 優先順序,最終標記到WIP節點上
   let newLanes = NoLanes;
   // 宣告newBaseState,注意接下來它被賦值的時機,還有前置條件:
   // 1. 當有優先順序被跳過,newBaseState賦值為newState,
   // 也就是queue.baseState
   // 2. 當都處理完成後沒有優先順序被跳過,newBaseState賦值為
   // 本輪新計算的state,最後更新到queue.baseState上
   let newBaseState = null;
   // 使用newFirstBaseUpdate 和 newLastBaseUpdate // 來表示本次更新產生的的baseUpdate佇列,目的是擷取現有佇列中
   // 第一個被跳過的低優先順序update到最後的所有update,最後會被更新到
   // updateQueue的firstBaseUpdate 和 lastBaseUpdate上
   // 作為下次渲染的遺留佇列(baseUpdate)
   let newFirstBaseUpdate = null;
   let newLastBaseUpdate = null;
   // 從頭開始迴圈
   let update = firstBaseUpdate;
   do {
     const updateLane = update.lane;
     const updateEventTime = update.eventTime;
     
     // isSubsetOfLanes函式的意義是,判斷當前更新的優先順序(updateLane)
     // 是否在渲染優先順序(renderLanes)中如果不在,那麼就說明優先順序不足
     if (!isSubsetOfLanes(renderLanes, updateLane)) {
       const clone: Update<State> = {
       eventTime: updateEventTime,
       lane: updateLane,
       suspenseConfig: update.suspenseConfig,
       tag: update.tag,
       payload: update.payload,
       callback: update.callback,
       next: null,
     };
     
     // 優先順序不足,將update新增到本次的baseUpdate佇列中
     if (newLastBaseUpdate === null) {
        newFirstBaseUpdate = newLastBaseUpdate = clone;
        // newBaseState 更新為前一個 update 任務的結果,下一輪
        // 持有新優先順序的渲染過程處理更新佇列時,將會以它為基礎進行計算。
        newBaseState = newState;
     } else {
       // 如果baseUpdate佇列中已經有了update,那麼將當前的update
       // 追加到佇列尾部
       newLastBaseUpdate = newLastBaseUpdate.next = clone;
     }
     /* *
      * newLanes會在最後被賦值到workInProgress.lanes上,而它又最終
      * 會被收集到root.pendingLanes。
      *  再次更新時會從root上的pendingLanes中找出渲染優先順序(renderLanes),
      * renderLanes含有本次跳過的優先順序,再次進入processUpdateQueue時,
      * update的優先順序符合要求,被更新掉,低優先順序任務因此被重做
      * */
      newLanes = mergeLanes(newLanes, updateLane);
 } else {
   if (newLastBaseUpdate !== null) {
     // 進到這個判斷說明現在處理的這個update在優先順序不足的update之後,
     // 原因有二:
     // 第一,優先順序足夠;
     // 第二,newLastBaseUpdate不為null說明已經有優先順序不足的update了
     // 然後將這個高優先順序放入本次的baseUpdate,實現之前提到的從updateQueue中
     // 擷取低優先順序update到最後一個update
     const clone: Update<State> = {
        eventTime: updateEventTime,
        lane: NoLane,
         suspenseConfig: update.suspenseConfig,
         tag: update.tag,
         payload: update.payload,
         callback: update.callback,
         next: null,
   };
   newLastBaseUpdate = newLastBaseUpdate.next = clone;
 }
 markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig);
 
 // 處理更新,計算出新結果
 newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, );
 const callback = update.callback;
 
 // 這裡的callback是setState的第二個引數,屬於副作用,
 // 會被放入queue的副作用佇列裡
 if (callback !== null) {
     workInProgress.effectTag |= Callback;
     const effects = queue.effects;
     if (effects === null) {
         queue.effects = [update];
     } else {
        effects.push(update);
     }
   }
 } // 移動指標實現遍歷
 update = update.next;
 
 if (update === null) {
   // 已有的佇列處理完了,檢查一下有沒有新進來的,有的話
   // 接在已有佇列後邊繼續處理
   pendingQueue = queue.shared.pending;
   if (pendingQueue === null) {
     // 如果沒有等待處理的update,那麼跳出迴圈
     break;
   } else {
     // 如果此時又有了新的update進來,那麼將它接入到之前合併好的佇列中
     const lastPendingUpdate = pendingQueue;
     const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
     lastPendingUpdate.next = null;
     update = firstPendingUpdate;
     queue.lastBaseUpdate = lastPendingUpdate;
     queue.shared.pending = null;
     }
  }
} while (true);
   // 如果沒有低優先順序的更新,那麼新的newBaseState就被賦值為
   // 剛剛計算出來的state
   if (newLastBaseUpdate === null) {
    newBaseState = newState;
   }
   // 完成階段------------------------------------
   queue.baseState = ((newBaseState: any): State);
   queue.firstBaseUpdate = newFirstBaseUpdate;
   queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes);
   workInProgress.lanes = newLanes; workInProgress.memoizedState = newState;
   }
 }
hooks中useReducer處理更新計算狀態的邏輯與此處基本一樣。

總結

經過上面的梳理,可以看出來整個對更新的處理都是圍繞優先順序。整個processUpdateQueue函式要實現的目的是處理更新,但要保證更新按照優先順序被處理的同時,不亂陣腳,這是因為它遵循一套固定的規則:優先順序被跳過後,記住此時的狀態和此優先順序之後的更新佇列,並將佇列備份到current節點,這對於update物件按次序、完整地被處理至關重要,也保證了最終呈現的處理結果和使用者的行為觸發的互動的結果保持一致。

歡迎掃碼關注公眾號,發現更多技術文章

相關文章