淺入深出setState(下篇)

上篇: https://segmentfault.com/a/11...

Part one - 同步 or 異步

1. 先說現象吧:

在React中,若是是由React引起的事件處理(好比經過onClick引起的合成事件處理)和組件生命週期函數內(好比componentDidMount),調用this.setState不會同步更新this.state,除此以外的setState調用會同步執行this.state。
所謂「除此以外」,指的是繞過React經過addEventListener直接添加的事件處理函數,還有經過setTimeout/setInterval產生的異步調用。react

若是咱們按照教科書般的方式來使用React,基本上不會觸及所謂的「除此以外」狀況。

2. 再說爲何會這樣:(其實只要解開了這個疑問,才能明白setState的異步隊列是如何實現的)

在React的setState函數實現中,會根據一個變量isBatchingUpdates判斷是直接更新this.state仍是放到隊列中回頭再說,而isBatchingUpdates默認是false,也就表示setState會同步更新this.state,可是,有一個函數batchedUpdates,這個函數會把isBatchingUpdates修改成true,而當React在調用事件處理函數和自身生命週期以前就會調用這個batchedUpdates,形成的後果,就是由React控制的事件處理過程和生命週期中的同步代碼調用setState不會同步更新this.statewebpack

注意:同步代碼調用,在合成事件和生命週期內的異步調用setState(好比ajax和setTimeout內),也是會同步更新this.setState。

Demo請看上篇的Part Fourgit

因此按照正常React用法都是會通過batchingUpdate方法的。這是因爲React有一套自定義的事件系統和生命週期流程控制,使用原生事件監聽和settimeout這種方式會跳出React這個體系,因此會直接更新this.state。github

咱們在看代碼是如何實現的,須要瞭解這樣一個東西「事務」,React內部的工具方法實現了一個可供使用的事務。web

Part two - React中的事務

React中的事務借用了計算機專業術語的單詞Transaction 。對比數據庫的事務性質,二者之間有共同點卻又不是一回事,簡答來講 把須要執行的方法用一個容器封裝起來,在容器內執行方法的先後,分別執行init方法和close方,其次來講,一個容器能夠包裹另外一個容器,這點又相似於洋蔥模型。ajax

React的合成事件系統和生命週期就使用了React內部實現的事務,爲其函數附加了先後兩個相似npm腳本pre和post兩個鉤子的事件。數據庫

這是一個npm srcipt的例子:npm

"prebuild": "echo I run before the build script",
"build": "cross-env NODE_ENV=production webpack",
"postbuild": "echo I run after the build script"

//用戶執行npm run build就會實際執行
npm run prebuild && npm run build && npm run postbuild

//所以能夠在兩個鉤子裏作一些準備工做和清理工做。

有過有興趣,咱們來看一下如何簡單使用事務:https://codesandbox.io/s/6xl5yrjvzzsegmentfault

var MyTransaction = function () {
  //...
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function () {
    return [
      {
        initialize: function () {
          console.log("before method perform");
        },
        close: function () {
          console.log("after method perform");
        }
      }
    ];
  }
});

var transaction = new MyTransaction();

transaction.reinitializeTransaction()

var testMethod = function () {
  console.log("test");
};

transaction.perform(testMethod);

Part three - 哪有什麼歲月靜好,不過是有人爲你負重前行

因此,咱們能夠獲得啓發,React的事件系統和生命週期事務先後的鉤子對isBatchingUpdates作了修改,其實就是在事務的前置pre內調用了batchedUpdates方法修改了變量爲true,而後在後置鉤子又置爲false,而後發起真正的更新檢測,而事務中異步方法運行時候,因爲JavaScript的異步機制,異步方法(setTimeout等)其中的setState運行時候,同步的代碼已經走完,後置鉤子已經把isBatchingUpdates設爲false,因此此時的setState會直接進入非批量更新模式,表如今咱們看來成爲了同步SetState。數組

嘗試在描述一下:整個React的每一個生命週期和合成事件都處在一個大的事務當中。原生綁定事件和setTimeout異步的函數沒有進入React的事務當中,或者是當他們執行時,剛剛的事務已經結束了,後置鉤子觸發了,close了。(你們能夠想想分別是哪種狀況)。

React「坐」在頂部調用堆棧框架並知道全部React事件處理程序什麼時候運行,setState在React管理的合成事件或者生命週期中調用,它會啓用批量更新事務,進入了批量更新模式,全部的setState的改變都會暫存到一個隊列,延遲到事務結束再合併更新。若是setState在React的批量更新事務外部或者以後調用,則會當即刷新。

懂得了事務,再回看,就明白,其實setState歷來都是同步運行,不過是React利用事務工具方法模擬了setState異步的假象。

延遲隊列如何實現,其實有悟性的同窗已經能夠大概猜到。咱們再來捋一捋,看源碼是否能驗證咱們的結論描述和現象。

Part four - 源碼驗證

能夠對照這張圖先來看下setState的流程代碼,代碼倉庫在個人github的react-source倉庫,目錄已被我精簡,剩下關鍵的源代碼文件夾。

圖片描述

首先,咱們搜索setState =看下setState何處被賦值,找到了這裏

// src/isomorphic/modern/class/ReactComponent.js
/*
 * React組件繼承自React.Component,而setState是React.Component的方法,
 * 所以對於組件來說setState屬於其原型方法,首先看setState的定義:   
 */
ReactComponent.prototype.setState = function(partialState, callback) {
    // 忽略掉入參驗證和開發拋錯
   //調用setState實際是調用了enqueueSetState
   // 調用隊列的入隊方法,把當前組件的示例和state存進入
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    // 若是有回調,把回調存進setState隊列的後置鉤子
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

會發現調用setState實際是調用this.updater.enqueueSetState,此時咱們不得不看一看updater及其enqueueSetState方法是什麼東西,咱們在當前文件搜索:

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // updater有默認值,真實運行時會注入,其實也算依賴注入
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactNoopUpdateQueue是一個這樣的對象,提供了基本的無效方法,真正的updater只有在React被真正加載前纔會被注入進來,運行時注入,嚴格來講是依賴注入,是React源碼的風格之一。

// src/isomorphic/modern/class/ReactNoopUpdateQueue.js
var ReactNoopUpdateQueue={
    isMounted: function(publicInstance) {
    return false;
  },
  enqueueCallback: function(publicInstance, callback) { },
  enqueueForceUpdate: function(publicInstance) { },
  enqueueReplaceState: function(publicInstance, completeState) { },
  enqueueSetState: function(publicInstance, partialState) { },
}

真實的enqueueSetState在這個文件內,方法把將要修改的state存入組件實例的internalInstance數組中,這裏就是state的延遲更新隊列了。而後立馬調用了一個全局的ReactUpdates.enqueueUpdate(internalInstance)方法。

// src/renderers/shared/reconciler/ReactUpdateQueue.js

  // 這個是setState真正調用的函數
  enqueueSetState: function(publicInstance, partialState) {
       // 忽略基本的容錯和拋錯
    // 存入組件實例,準備更新
    var internalInstance = publicInstance;
    // 更新隊列合併操做 更新 internalInstance._pendingStateQueue
    var queue = internalInstance._pendingStateQueue ||(internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
    // 發生了什麼?猜一下?   ReactUpdates.js
  },
題外話:每一個函數的健壯入參判斷和運行環境判斷和完善的拋錯機制,對來源不信任甚至是內部互調,是React對開發者友好的一個重要緣由。

咱們來猜下ReactUpdates.enqueueUpdate幹了什麼?根據上面的流程圖我猜測應當是判斷流程。

function enqueueUpdate(component) {
  ensureInjected(); //環境判斷:是否有調度事務方法同時有批量更新策略方法
  //關鍵的判斷條件,是不是批量更新
  //但是isBatchingUpdates這個值誰來維護呢?
  if (!batchingStrategy.isBatchingUpdates) {        // 見詞知意 若是不在批量更新策略中
    // 若是不是批量更新,猜測一下,應該會當即更新吧?
    // 唉?batchingStrategy到底在作什麼呢
    batchingStrategy.batchedUpdates(enqueueUpdate, component);  // 調用事務
    // 對隊列中的更新執行 batchedUpdates 方法
    return;
  }
  // 若是是批量更新,那就把組件放入髒組件隊列,也就是待更新組件隊列
  dirtyComponents.push(component);
}

須要看ReactDefaultBatchingStrategy.js 看 batchedUpdates 方法,這個js文件就有意思了,一上來就是咱們以前提到的事務。

避免枯燥,我用人話闡述一下這個js的內容,也能夠直接看ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy={
    isBatchingUpdates:false,
    batchedUpdates:function(){},
}

文件底部聲明瞭 ReactDefaultBatchingStrategy對象,內部isBatchingUpdates初始值爲false,這個就是咱們心心念念判斷是否在批量更新策略的重要變量。
這個isBatchingUpdates變量搜索整個項目,發現它只被兩處改變:

  1. 對象自身的另外一個batchedUpdates方法固定賦值爲true,標識着開啓批量更新策略。
  2. 一個事務的close鉤子,設爲false,標識着結束批量更新策略。剛好,這個事務被batchedUpdates調用。

實質上,isBatchingUpdates僅僅也就是被batchedUpdates方法維護着,batchedUpdates調用時開啓批量更新,同時入參callback被事務包裹調用,callback調用完成時候事務close鉤子觸發,關閉批量更新模式。事務的close鉤子函數有兩個,另外一個以前會調用ReactUpdates.flushBatchedUpdates方法,也就是真正的把積攢的setState隊列進行更新計算。

問題來了,callback是啥,batchedUpdates方法在setState以前,或者說除了setState還會被誰調用,致使isBatchingUpdates變爲true,我猜測是生命週期函數和合成事件,只有這樣,整個維護批量更新策略的機制就造成了閉環,驗證了咱們以前的結論。

咱們搜索batchedUpdates(,果不其然,在src/renderers/dom/client/ReactEventListener.jssrc/renderers/dom/client/ReactMount.js中找到了ReactUpdates.batchedUpdates的調用。

合成事件和生命週期的裝載發生時,調用了batchedUpdates方法,使得內部的同步代碼均可以運行在批量更新策略的事務環境中,結束後,便使用事務的後置鉤子啓動merge更新,重置常量。

另外我在ReactDOM.js發現了對React頂層API對batchedUpdates方法的引用,可讓 Promise 這些異步也能進入 batch update:

unstable_batchedUpdates: ReactUpdates.batchedUpdates,

另外一個彩蛋,雖然React不提倡使用這個API,之後版本也可能移除,可是如今咱們能夠這樣在React中這樣使用:

React.unstable_batchedUpdates(function(){
    this.setState({...})
    this.setState({...})
    //...在此函數內也可使用批量更新策略
})

解決了setTimeout和AJAX異步方法、原生事件內的setState批量更新策略失效的問題,讓批量更新在任何場景都會發生。

Part five - 總結

圖片描述

  1. this.setState首先會把state推入pendingState隊列中。
  2. 而後將組建標記爲dirtyComponent。
  3. React中有事務的概念,最多見的就是更新事務,若是不在事務中,則會開啓一次新的更新事務,更新事務執行的操做就是把組件標記爲dirty。
  4. 判斷是否處於batch update。
  5. 是的話,保存組建於dirtyComponent中,在事務的時候纔會經過 ReactUpdates.flushBatchedUpdates 方法將全部的臨時 state merge 並計算出最新的 props 及 state,而後將其批量執行,最後再關閉結束事務。
  6. 不是的話,直接開啓一次新的更新事務,在標記爲dirty以後,直接開始更新組件。所以當setState執行完畢後,組件就更新完畢了,因此會形成定時器同步更新的狀況。
相關文章
相關標籤/搜索