基於React 原始碼深入淺出setState:深度刨析updater的結構和原理

墨成發表於2018-09-06

作者 : 墨成   React 版本 :16.4.1

閱讀本文之前,建議閱讀:

1.基於React 原始碼深入淺出setState:官方文件的啟示錄

2.基於React 原始碼深入淺出setState:setState非同步實現

上一篇 詳細瞭解了setState的一些機制和原理,同時對 updater 也作了簡單的解釋,這篇文章我們將詳細瞭解這個updater的資料結構和呼叫堆疊.

程式碼如下:

react\packages\react-reconciler\src\ReactFiberClassComponent.js
複製程式碼

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ReplaceState;
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'replaceState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
};複製程式碼

程式碼並不長,但是包含的資訊還是比較多,首先我們知道這個 updater 是個常量物件,而且在整個React它是單例的,即所有元件都是用同一個updater(注意與後面提及的update區別),updater中包含了3個非常重要的方法 enqueueSetState,enqueueForceUpdate,enqueueReplaceState 準確的說是包含了一個重要的方法 enqueueSetState

我們首先看下 在setState中呼叫的 enqueueSetState(inst, payload, callback)

this.updater.enqueueSetState(this, partialState, callback, 'setState');   複製程式碼

實際的 enqueueSetState(inst, payload, callback)只有三個引數,第四個引數是為預設的ReactNoopUpdateQueue 準備的,可以忽略。

/**
** @param {?react component} 元件事例 
* @param {object|function} payload 對應的就是 partialState
* @param {?function} callback 回撥函式 
 
*/
enqueueSetState(inst, payload, callback) {
  const fiber = ReactInstanceMap.get(inst);
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, fiber);

  const update = createUpdate(expirationTime);
  update.payload = payload;
  if (callback !== undefined && callback !== null) {
    if (__DEV__) {
      warnOnInvalidCallback(callback, 'setState');
    }
    update.callback = callback;
  }

  enqueueUpdate(fiber, update);
  scheduleWork(fiber, expirationTime);
}複製程式碼
  1. 前三個變數 fiber,currentTime,expirationTime

const fiber = ReactInstanceMap.get(inst);

ReactInstanceMap拿到Component的Fiber例項 ,它其實等於

const fiber = inst._reactInternalFiber; 

基於React 原始碼深入淺出setState:深度刨析updater的結構和原理

FB的工程師們通過 ReactInstanceMap這樣的實現應該是考慮到後續的擴充和對私有變數取值的封裝,後續 ReactInstanceMap 應該會使一個類似<key,value>的鍵值對資料型別 .

  const currentTime = requestCurrentTime();複製程式碼

React通過Performance.now()(如果瀏覽器不支援這個API ,則是用Date.now())獲取時間值(說明:返回當前日期和時間的 Date 物件與'1970/01/01 00:00:00'之間的毫秒值(北京時間的時區為東8區,起點時間實際為:'1970/01/01 08:00:00'),所以這個currentTime是個number型別,即typeof currentTime === 'number' )

  const expirationTime = computeExpirationForFiber(currentTime, fiber);複製程式碼

expirationTime根據當前時間和 fiber計算它的 有效時間.expirationTime 涉及的體系比較複雜,後續會結合實際的程式碼講解.

2.Update物件

    const update = createUpdate(expirationTime);
    update.payload = payload;複製程式碼

    建立一個 update 的物件,資料結構在下面會有提及,這裡的payLoad 還是partialState

    export function createUpdate(expirationTime: ExpirationTime): Update<*> {
      return {
        expirationTime: expirationTime,
    
        tag: UpdateState,
        payload: null,
        callback: null,
    
        next: null,
        nextEffect: null,
      };
    }複製程式碼

    3. enqueueUpdate&scheduleWork

    故事講到這裡,應該很快進入高潮,兩個比較核心的看點

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);複製程式碼

    在瞭解這兩個函式之前,我們首先看下兩個資料結構

    位置:react\packages\react-reconciler\src\ReactUpdateQueue.js
    
    export type Update<State> = {
      expirationTime: ExpirationTime,//有效時間
    
      tag: 0 | 1 | 2 | 3,//tag  1 2 3 4 其中之一 
      payload: any,//update的partialState
      callback: (() => mixed) | null,//setState的回撥函式
    
      next: Update<State> | null,//指向下一個 update的指標
      nextEffect: Update<State> | null,//指向下一個effect(變化) 
    };
    
    export type UpdateQueue<State> = {
      baseState: State,//就是Component初始化的state
    
      firstUpdate: Update<State> | null,
      lastUpdate: Update<State> | null,
    
      firstCapturedUpdate: Update<State> | null,
      lastCapturedUpdate: Update<State> | null,
    
      firstEffect: Update<State> | null,
      lastEffect: Update<State> | null,
    
      firstCapturedEffect: Update<State> | null,
      lastCapturedEffect: Update<State> | null,
    };複製程式碼

    每個update有個指向下一個update的"指標" next,  UpdateQueue<State> 是個單向連結串列,firstxxx  lastxxx分別指向連結串列頭部和尾部. enqueueUpdate(fiber, update),所以很多人錯誤的認為updateQuenue 是一個(類)陣列,  程式碼如下 :

    //以下注釋僅對 ClassComponent有效(即你的元件是繼承React.Component)
    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
      // Update queues are created lazily. Update queues被延遲建立(即這個元件沒有update 我們沒有必要給它fiber來建立這樣屬性) 
      const alternate = fiber.alternate;
      let queue1;//設計它是為了指向current的updateQueue
      let queue2; //設計它是為了指向alternate 的updateQueue
      if (alternate === null) {//alternate為null,對於classcomponent第一次呼叫setState時alternate為null
        // There's only one fiber.
        queue1 = fiber.updateQueue;
        queue2 = null;
        if (queue1 === null) {//首次呼叫setState建立updateQueue,這時的memoizedState為初始值
          queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        }
      } else {
        // There are two owners.
        queue1 = fiber.updateQueue;//current Fiber updateQueue 
        queue2 = alternate.updateQueue;
        if (queue1 === null) {
          if (queue2 === null) {
            // Neither fiber has an update queue. Create new ones.
            //如果都為空,則分別建立,這個case我沒有找到
            queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
            queue2 = alternate.updateQueue = createUpdateQueue(
              alternate.memoizedState,
            );
          } else {
            // Only one fiber has an update queue. Clone to create a new one.
            // 如果有一個有update Queue,則克隆一個 
            queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
          }
        } else {
          if (queue2 === null) {
            // Only one fiber has an update queue. Clone to create a new one.
            queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
          } else {
            // Both owners have an update queue.
          }
        }
      }
      if (queue2 === null || queue1 === queue2) { 
        // There's only a single queue.
        //一般發生在首次 setState
        appendUpdateToQueue(queue1, update);
      } else {
        // There are two queues. We need to append the update to both queues,
        // while accounting for the persistent structure of the list — we don't
        // want the same update to be added multiple times.
         // 翻譯:如果存在兩個queues,我們需要追加這個 update到這個兩個 queues.
          //然而對於這種永續性結構的列表(updateQueue)需要保證一次update不能新增多次 
        if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
          // One of the queues is not empty. We must add the update to both queues.
          appendUpdateToQueue(queue1, update);
          appendUpdateToQueue(queue2, update);
        } else {
          // Both queues are non-empty. The last update is the same in both lists,
          // because of structural sharing. So, only append to one of the lists.
          appendUpdateToQueue(queue1, update);
          // But we still need to update the `lastUpdate` pointer of queue2.
          queue2.lastUpdate = update;
        }
      }
    
      if (__DEV__) {
        if (
          fiber.tag === ClassComponent &&
          (currentlyProcessingQueue === queue1 ||
            (queue2 !== null && currentlyProcessingQueue === queue2)) &&
          !didWarnUpdateInsideUpdate
        ) {
          warningWithoutStack(
            false,
            'An update (setState, replaceState, or forceUpdate) was scheduled ' +
              'from inside an update function. Update functions should be pure, ' +
              'with zero side-effects. Consider using componentDidUpdate or a ' +
              'callback.',
          );
          didWarnUpdateInsideUpdate = true;
        }
      }
    }複製程式碼

    關於alternate ( 參考React Fiber Architecture) 

    基於React 原始碼深入淺出setState:深度刨析updater的結構和原理

    簡單的概括下:在任何時候,一個元件的例項最多應對兩個fiber:current,alternate(flushed fiber or work-in-progress fiber,不同階段的叫法),alternate是延遲建立並從 cloneFiber這個方法中克隆出來的 ,也就是所謂的persitent structure,他們結構共享(Structural Sharing).

    Structural Sharing 和 延遲建立貫穿整個React  ,比如這裡 

    FB 的工程師有段註釋:

    // Update queues are created lazily.複製程式碼

    update queues是延遲建立,要注意的是,這裡的延遲建立不僅僅是對current,對alternate的queue也是一樣. 

    scheduleWork 涉及排程器的知識體系,後續學習完成補充著部分的欠缺 。

    參考資料 :

    1.React Fiber Architercture


    相關文章