React16——看看setState過程中fiber幹了什麼事情

二丶月發表於2018-05-18

目的

通過觀察呼叫棧和其他部落格的介紹親身體驗下setState過程中fiber幹了什麼事情

1、create-react-app建立一個demo

下圖是一個典型的create-react-app建立的專案,其中Text.js是我新增的子元件,在App.js中引用到。

clipboard.png

2、修改App.js檔案和新增Text.js檔案

App.js

import React, { Component } from 'react';
import { Text } from './Text'

class App extends Component {
    constructor(props) {
        super(props)
        this.state = {
            tab: 'Welcome to React'
        }
    }
    updateState = () => {
        this.setState(() => ({tab: 'Bye Bye'}))
    }
  render() {
    return (
      <div className="App">
          <Text tab={this.state.tab} updateState={this.updateState} />
      </div>
    );
  }
}
export default App;

複製程式碼

Text.js

import React from 'react'

export const Text = (props) => {
    return (
        <span onClick={() => props.updateState()}>{props.tab}</span>
    )
}
複製程式碼

3、執行setState

state.tab的初始值是'Welcome to React',在setState中,傳入了一個箭頭函式,去更新state.tab的值為'Bye Bye',

//partialState為() => ({tab: ''Bye Bye}),callback沒有傳入
Component.prototype.setState = function (partialState, callback) {
  //this為當前元件的例項
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製程式碼

接著,執行了updater上的enqueueSetState方法,每一個例項都會有一個updater(更新器),updater的作用在下面介紹,在當前App元件例項中,_reactInternalFiber是當前元件的fiber,而_reactInternalInstance是在react15使用的物件。

clipboard.png

4、updater更新器

updater有3個方法,只用關心enqueueSetState

var updater = {
    isMounted: isMounted,
    /*
    * instance: 上一步傳入的App元件例項,
    * partialState:需要執行更新的箭頭函式,
    * callback:undefined
    */
    enqueueSetState: function (instance, partialState, callback) {
      //獲取到當前例項上的fiber
      var fiber = get(instance);
      //計算當前fiber的到期時間(優先順序)
      var expirationTime = computeExpirationForFiber(fiber);
      //一次更新需要的配置引數
      var update = {
        expirationTime: expirationTime, //優先順序
        partialState: partialState, //更新的state,通常是函式而不推薦物件寫法
        callback: callback, //更新之後執行的回撥函式
        isReplace: false, //
        isForced: false, //是否強制更新
        capturedValue: null, //捕獲的值
        next: null, //
      };
      //將update上需要更新的資訊新增到fiber中
      insertUpdateIntoFiber(fiber, update);
      //排程器排程fiber任務
      scheduleWork(fiber, expirationTime);
    },
    //替換更新state,不關注
    enqueueReplaceState: function (instance, state, callback) {},
    //執行強制更新state,不關注
    enqueueForceUpdate: function (instance, callback) {}
  };
複製程式碼

下面按步驟詳細看看這個函式內部的執行流程。

獲取fiber:key === instance,fiber很重要,記錄了很多有用的資訊,比如當前元件例項的各種屬性和狀態、優先順序、標識等。

function get(key) {
  return key._reactInternalFiber;
}
複製程式碼

也許你會很好奇fiber長什麼樣,圖上展示的是元件例項上注入的fiber資料結構。

clipboard.png

計算到期時間:也就是計算當前fiber任務的優先順序,從程式碼上看需要判斷的條件比較多,既可以是非同步更新,也可以是同步更新。在當前測試中,進入的是同步更新的流程。而同步對應的優先順序就是1,所以expirationTime = 1。

 //用來計算fiber的到期時間,到期時間用來表示任務的優先順序。
 function computeExpirationForFiber(fiber) {
    var expirationTime = void 0;
    if (expirationContext !== NoWork) {
      // 
      expirationTime = expirationContext;
    } else if (isWorking) {
      if (isCommitting) {
        //同步模式,立即處理任務,預設是1
        expirationTime = Sync;
      } else {
        //渲染階段的更新應該與正在渲染的工作同時過期。
        expirationTime = nextRenderExpirationTime;
      }
    } else {
      //沒有到期時間的情況下,建立一個到期時間
      if (fiber.mode & AsyncMode) {
        if (isBatchingInteractiveUpdates) {
          // 這是一個互動式更新
          var currentTime = recalculateCurrentTime();
          expirationTime = computeInteractiveExpiration(currentTime);
        } else {
          // 這是一個非同步更新
          var _currentTime = recalculateCurrentTime();
          expirationTime = computeAsyncExpiration(_currentTime);
        }
      } else {
        // 這是一個同步更新
        expirationTime = Sync;
      }
    }
    if (isBatchingInteractiveUpdates) {
      //這是一個互動式的更新。跟蹤最低等待互動過期時間。這允許我們在需要時同步重新整理所有互動更新。
      if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
        lowestPendingInteractiveExpirationTime = expirationTime;
      }
    }
    return expirationTime;
  }
複製程式碼

將update上需要更新的資訊新增到fiber中:這個函式的作用就是把我們在上面通過計算之後得到的update更新到fiber上面,實際操作是物件的賦值,跟合併是一個意思。

function insertUpdateIntoFiber(fiber, update) {
  //確保更新佇列存在,不存在則建立
  ensureUpdateQueues(fiber);
  //上一步已經將q1和q2佇列進行了處理,定義2個區域性變數queue1和queue2來儲存佇列資訊。
  var queue1 = q1;
  var queue2 = q2;
  // 如果只有一個佇列,請將更新新增到該佇列並退出。
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // 如果任一佇列為空,我們需要新增到兩個佇列中。
  if (queue1.last === null || queue2.last === null) {
    //將update的值更新到佇列1和佇列2上,然後退出該函式
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // 如果兩個列表都不為空,則由於結構共享,兩個列表的最後更新都是相同的。所以,我們應該只追加到其中一個列表。
  insertUpdateIntoQueue(queue1, update);
  // 但是我們仍然需要更新queue2的`last`指標。
  queue2.last = update;
}
複製程式碼

初始化的時候,fiber中的updateQueue是null,這時候,就要建立createUpdateQueue一個更新佇列。alternate本質上也是fiber,它記錄的是上一次setState操作的fiber,同時alternate又是fiber的一個屬性。

clipboard.png

ensureUpdateQueues的作用是確保更新佇列不為null。

var q1 = void 0;
var q2 = void 0;
function ensureUpdateQueues(fiber) {
  q1 = q2 = null;
  // 我們將至少有一個和最多兩個不同的更新佇列。
  //alternate是fiber上的一個屬性,初始化是null,執行了setState的過程中,會將當前的FiberNode儲存到alternate上,下次setState時,就能讀取到,可以用來做狀態回滾。
  var alternateFiber = fiber.alternate;
  var queue1 = fiber.updateQueue;
  if (queue1 === null) {
    // 沒有佇列,就建立佇列
    queue1 = fiber.updateQueue = createUpdateQueue(null);
  }

  var queue2 = void 0;
  if (alternateFiber !== null) {
    queue2 = alternateFiber.updateQueue;
    if (queue2 === null) {
      queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
    }
  } else {
    queue2 = null;
  }
  queue2 = queue2 !== queue1 ? queue2 : null;

  // 使用模組變數
  q1 = queue1;
  q2 = queue2;
}
複製程式碼
//將update的值更新到queue中。
function insertUpdateIntoQueue(queue, update) {
  // 將更新附加到列表的末尾。
  if (queue.last === null) {
    // 佇列是空的
    queue.first = queue.last = update;
  } else {
    queue.last.next = update;
    queue.last = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
    queue.expirationTime = update.expirationTime;
  }
}
複製程式碼

scheduleWork進行排程:上面的幾個步驟記得做了什麼嗎?拿到元件例項上的fiber,然後通過計算得到優先順序和其他需要更新的fiber屬性,最後更新到fiber上,同時建立了更新佇列。但是react還沒開始幹活是不是,更新佇列有了,fibre也有了,react的大腦該對fiber進行排程了。

排程的邏輯很複雜,因為影響因素太多了,我無法一一列舉,只能根據當前的呼叫棧識別用到的部分。

//傳入2個引數,fiber和優先順序,內部又巢狀了一個函式scheduleWorkImpl,這個函式才是邏輯部分。
function scheduleWork(fiber, expirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
  }
複製程式碼

請注意當前傳入的fiber是合併了update屬性之後的fiber。

scheduleWorkImpl有寫讓人迷惑,司徒大佬的文章也沒有解釋清楚這個函式,一步步來看的話,recordScheduleUpdate的作用是先判斷當前有沒有正在提交更新或者已經在更新中的任務,應該是等updater執行完後,要用到的一些條件預設。

然後將node = fiber,別糾結為什麼是直接相等,接著執行迴圈,當前node也就是fiber不為空,根據條件,要在迴圈過程中對node清空,清空之後退出函式。那麼,這個清空的過程做了什麼事情呢?

先是判斷node裡面的到期時間是不是等於NoWork,NoWork表示的是0,它表示的是當前沒有在排程中的fiber,然後判斷node的到期時間是不是大於傳入的到期時間,如果滿足條件,就將node的到期時間更新為新傳入的到期時間。

然後判斷alternate不為空的情況下,alternate在沒有執行過setState,通常是初始化的時候是空狀態,當執行過一次setState之後,就會將舊的FiberNode賦值給alternate,下面的函式中,如果alternate不為空,並且expirationTime和上一個if的判斷一致的情況下,就更新alternate中的expirationTime。

上2個條件是更新到期時間的,第3個條件是判斷return是不是等於null,return的含義在完全理解fiber一文中有說到,表示當前的fiber任務向誰提交。在本demo中,當前是第一次執行,所有它的return為null。

function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
    //記錄排程的狀態
    recordScheduleUpdate();
    var node = fiber;
    while (node !== null) {
      // 將父路徑移到根目錄並更新每個節點的到期時間。
      if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
        node.expirationTime = expirationTime;
      }
      if (node.alternate !== null) {
        if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
          node.alternate.expirationTime = expirationTime;
        }
      }
      if (node['return'] === null) {
        if (node.tag === HostRoot) {
          var root = node.stateNode;
          if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime) {
            // 是一箇中斷。 (用於效能跟蹤。)
            interruptedBy = fiber;
            resetStack();
          }
          if (
          // 如果我們處於渲染階段,我們不需要為此更新安排此根目錄,因為我們將在退出之前執行此操作。
          !isWorking || isCommitting ||
          // ......除非這是與我們渲染的根不同的root。
          nextRoot !== root) {
            // 將root新增到root排程
            requestWork(root, expirationTime);
          }
        } else {
          return;
        }
      }
      node = node['return'];
    }
  }
複製程式碼
//顧名思義,記錄排程的狀態
function recordScheduleUpdate() {
  if (enableUserTimingAPI) {
    if (isCommitting) {
      //當前是否有正在提交的排程任務,肯定沒有啦。
      hasScheduledUpdateInCurrentCommit = true;
    }
    //currentPhase表示當前執行到哪個生命週期了。
    if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
      //當前是否有排程到某個生命週期階段的任務
      hasScheduledUpdateInCurrentPhase = true;
    }
  }
}
複製程式碼

到這裡為止,updater的函式執行完了,我們總結一下它到底做了什麼事情。一共有4點:

  • 找到例項上的fiber
  • 計算得到當前fiber的優先順序
  • 將要更新的fiber推送到更新佇列
  • 根據fiber樹上的優先順序確定更新工作,從當前fiber的return為起點,開始遞迴,直至到達根節點,根節點的return=null。

後續

根據呼叫棧,我們看到了setState函式的執行過程,但是此時並沒有在瀏覽器上看到更新,因為具體的排程工作還是要依靠react的核心演算法去執行,updater只是將fiber更新到佇列中,和確定了更新的優先順序。

後面要經歷react的事件合成,Diff演算法,虛擬DOM解析,生命週期執行等過程。非常多的程式碼,要是一一解釋,可以寫一本書出來了。

以後要是有時間,可以將後半部分關於setState裡的() => ({tab: 'Bye Bye'})是如何更新的說說。 一切都要從createWorkInProgress(current, pendingProps, expirationTime)開始說起。

無論是國內哪位大神的部落格,只要是介紹fiber的,萬變不離其宗,看國外的這篇文章:fiber詳解

Bye Bye,各位。

臥槽,這是什麼

clipboard.png

相關文章