寫給本身看的React源碼解析(二):setState是同步仍是異步的?

前言

這個問題相信是大部分的人剛開始學習React的時候,第一個碰到的問題,對我來講,學習的教程裏就只是告訴我直接使用setState是異步的,可是在一些好比setTimeout這樣的異步方法裏,它是同步的。數組

我那時候就很疑惑,雖然想要一探究竟,可是爲了儘快上手React,仍是以應用優先,留到以後的源碼學習時,再來深刻了解一下。緩存

本文的內容就是從源碼層面來分析setState究竟是同步仍是異步的。由於如今作的項目其實都是使用hooks在維護數據狀態,對於class使用特別少,因此我並不會太過深究底層的渲染原理,重點只是在於爲何setState有的時候表現是同步,有的時候表現是異步。性能優化

例子

export default class App extends React.Component{
  state = {
    num: 0
  }
  add = () => {
    console.log('add前', this.state.num)
    this.setState({
      num: this.state.num + 1
    });
    console.log('add後', this.state.num)
  }
  add3 = () => {
    console.log('add3前', this.state.num)
    this.setState({
      num: this.state.num + 1
    });
    this.setState({
      num: this.state.num + 1
    });
    this.setState({
      num: this.state.num + 1
    });
    console.log('add3後', this.state.num)
  }
  reduce = () => {
    setTimeout(() => {
      console.log('reduce前', this.state.num)
      this.setState({
        num: this.state.num - 1
      });
      console.log('reduce後', this.state.num)
    },0);
  }
  render () {
    return <div> <button onClick={this.add}>點擊加1</button> <button onClick={this.add3}>點擊加3次</button> <button onClick={this.reduce}>點我減1</button> </div>
  }
}
複製代碼

按順序依次點擊這三個按鈕,咱們來看下控制檯打印出來的內容。markdown

這個結果,對於有點經驗React開發者來講很簡單。app

按照這個例子來看,setState是一個異步的方法,執行完成以後,數據不會立刻修改,會等到後續某個時刻才進行變化。屢次調用setState,只會執行最新的那個事件。在異步的方法中,它會有同步的特性。異步

咱們先不着急下結論,咱們深刻setState的流程中去找結論。ide

異步的原理-批量更新

出於性能優化的須要,一次setState是不會觸發一個完整的更新流程的,在一個同步的代碼運行中,每次執行一個setState,React會把它塞進一個隊列裏,等時機成熟,再把「攢起來」的state結果作合併,最後只針對最新的state值走一次更新流程。這個過程,叫做批量更新函數

這樣子,就算咱們代碼寫的再爛,好比寫了一個循環100次的方法,每次都會調用一個setState,也不會致使頻繁的re-render形成頁面的卡頓。性能

這個原理,解釋了上面第一個按鈕以及第二個按鈕的現象。學習

同步的原理-setState工做流

這裏的問題就一個,爲何setTimeout能夠將setState的執行順序從異步變爲同步?

咱們來看看setState的源碼

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
複製代碼

不考慮callback回調,這裏其實就是觸發了一個enqueueSetState方法。

enqueueSetState: function (publicInstance, partialState) {
  // 根據 this 拿到對應的組件實例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  // 這個 queue 對應的就是一個組件實例的 state 數組
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);
  // enqueueUpdate 用來處理當前的組件實例
  enqueueUpdate(internalInstance);
}
複製代碼

這個方法就是剛纔說的,把state的修改放進隊列中。而後使用enqueueUpdate來處理將要更新的組件實例。再來看看enqueueUpdate方法。

function enqueueUpdate(component) {
  ensureInjected();
  // 注意這一句是問題的關鍵,isBatchingUpdates標識着當前是否處於批量建立/更新組件的階段
  if (!batchingStrategy.isBatchingUpdates) {
    // 若當前沒有處於批量建立/更新組件的階段,則當即更新組件
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 不然,先把組件塞入 dirtyComponents 隊列裏,讓它「再等等」
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
複製代碼

這裏重點關注一個對象,batchingStrategyReact內部專門用於管控批量更新的對象),它的屬性isBatchingUpdates直接決定了當下是否要走更新流程,仍是應該等等。

每當React調用batchedUpdate去執行更新動做時,會先把這個鎖給「鎖上」(置爲true),代表「如今正處於批量更新過程當中」。當鎖被「鎖上」的時候,任何須要更新的組件都只能暫時進入dirtyComponents裏排隊等候下一次的批量更新。

var ReactDefaultBatchingStrategy = {
  // 全局惟一的鎖標識
  isBatchingUpdates: false,
 
  // 發起更新動做的方法
  batchedUpdates: function(callback, a, b, c, d, e) {
    // 緩存鎖變量
    var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
    // 把鎖「鎖上」
    ReactDefaultBatchingStrategy. isBatchingUpdates = true

    if (alreadyBatchingStrategy) {
      callback(a, b, c, d, e)
    } else {
      // 啓動事務,將 callback 放進事務裏執行
      transaction.perform(callback, null, a, b, c, d, e)
    }
  }
}
複製代碼

這裏,咱們還須要瞭解React中的Transaction(事務) 機制。

TransactionReact源碼中表現爲一個核心類,Transaction能夠建立一個黑盒,該黑盒可以封裝任何的方法。所以,那些須要在函數運行前、後運行的方法能夠經過此方法封裝(即便函數運行中有異常拋出,這些固定的方法仍可運行),實例化Transaction時只需提供相關的方法便可。

* <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre>
複製代碼

Transaction就像是一個「殼子」,它首先會將目標函數用wrapper(一組initializeclose方法稱爲一個wrapper) 封裝起來,同時須要使用Transaction類暴露的perform方法去執行它。如上面的註釋所示,在anyMethod執行以前,perform會先執行全部 wrapperinitialize方法,執行完後,再執行全部wrapperclose方法。這就是React中的事務機制。

結合咱們剛纔的點擊事件,事件實際上是做爲一個callback回調函數在事務中調用的,調用以前,批量更新策略事務會把isBatchingUpdates置爲true,而後執行callback方法,執行完畢以後,把isBatchingUpdates置爲false,而後再循環全部的dirtyComponents調用updateComponent更新組件。

因此剛纔的點擊事件,其實能夠這樣理解

add = () => {
  // 進來先鎖上
  isBatchingUpdates = true
  console.log('add前', this.state.num)
  this.setState({
    num: this.state.num + 1
  });
  console.log('add後', this.state.num)
  // 執行完函數再放開
  isBatchingUpdates = false
}
複製代碼

這種狀況下,setState是異步的。

咱們再來看看setTimeout的狀況

reduce = () => {
  // 進來先鎖上
  isBatchingUpdates = true
  setTimeout(() => {
    console.log('reduce前的', this.state.num)
    this.setState({
      num: this.state.num - 1
    });
    console.log('reduce後的', this.state.num)
  },0);
  // 執行完函數再放開
  isBatchingUpdates = false
}
複製代碼

由於setTimeout是在以後的宏任務中執行的,因此這時候運行的setStateisBatchingUpdates已經被置爲false了,它會當即執行更新,因此具有了同步的特性。setState 並非具有同步這種特性,只是在某些特殊的執行順序下,脫離了異步的控制

總結

setState並非單純同步/異步的,它的表現會因調用場景的不一樣而不一樣:在React鉤子函數及合成事件中,它表現爲異步;而在 setTimeoutsetInterval等函數中,包括在DOM原生事件中,它都表現爲同步。這種差別,本質上是React事務機制和批量更新機制的工做方式來決定的。

相關文章
相關標籤/搜索