iT邦幫忙

0

React狀態計算解密

nero 2021-01-24 22:46:381574 瀏覽
  • 分享至 

  • xImage
  •  

點擊進入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對象按次序、完整地被處理至關重要,也保證了最終呈現的處理結果和用戶的行為觸發的交互的結果保持壹致。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言