React setState 整理總結

寫業務代碼的時候 須要常常用到setState, 前幾天review代碼的時候, 又想了一下這個API, 發現對它的瞭解不是很清楚, 僅僅是 setState 是異步的, 週六在家參考了一些資料,簡單整理了下,寫的比較簡單, 通篇閱讀大概耗時 5min, 在這簡單分享一下, 但願對你們有所幫助 ;)。html

先看一個例子

假若有這樣一個點擊執行累加場景:前端

this.state = {
  count: 0,
}

incrementCount() {
  this.setState({
    count: this.state.count + 1,
  });
}

handleIncrement = () => {
 this.incrementCount();
 this.incrementCount();
 this.incrementCount();
}

每一次點擊, 累加三次,看一下輸入:react

並無達到預期的效果,糾正也很簡單:數組

incrementCount() {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

再看輸出:app

setState 的時候, 一個傳入了object, 一個傳入了更新函數。異步

區別在於: 傳入一個更新函數,就能夠訪問當前狀態值。 setState調用是 批量處理的,所以可讓更新創建在彼此之上,避免衝突。函數

那問題來了, 爲何前一種方式就不行呢? 帶着這個疑問,繼續往下看。性能

setState 爲何不會同步更新組件this

進入這個問題以前,咱們先回顧一下如今對 setState 的認知:spa

  • 1.setState 不會馬上改變React組件中state的值.
  • 2.setState 經過觸發一次組件的更新來引起重繪.
  • 3.屢次 setState 函數調用產生的效果會合併

重繪指的就是引發 React 的更新生命週期函數4個函數:

  • shouldComponentUpdate(被調用時this.state沒有更新;若是返回了false,生命週期被中斷,雖然不調用以後的函數了,可是state仍然會被更新)
  • componentWillUpdate(被調用時this.state沒有更新)
  • render(被調用時this.state獲得更新)
  • componentDidUpdate

若是每一次 setState 調用都走一圈生命週期,光是想想也會以爲會帶來性能的問題,其實這四個函數都是純函數,性能應該還好,可是render函數返回的結果會拿去作Virtual DOM比較和更新DOM樹,這個就比較費時間。

目前React會將setState的效果放在隊列中,積攢着一次引起更新過程。

爲的就是把 Virtual DOM 和 DOM 樹操做降到最小,用於提升性能

查閱一些資料後發現,某些操做仍是能夠同步更新 this.state的。

setState 何時會執行同步更新

先直接說結論吧:

在React中,若是是由React引起的事件處理(好比經過onClick引起的事件處理),調用 setState 不會同步更新 this.state,除此以外的setState調用會同步執行this.state。

所謂「除此以外」,指的是繞過React經過
addEventListener
直接添加的事件處理函數,還有經過
setTimeout || setInterval

產生的異步調用。

簡單一點說, 就是通過React 處理的事件是不會同步更新 this.state的. 經過 addEventListener || setTimeout/setInterval 的方式處理的則會同步更新

具體能夠參考 jsBin 的這個例子。

結果就很清晰了:

點擊Increment ,執行onClick ,輸出0;
而經過addEventListener , 和 setTimeout 方式處理的, 第一次 直接輸出了1;

理論大概是這樣的,盜用一張圖:

clipboard.png

在React的setState函數實現中,會根據一個變量 isBatchingUpdates 判斷是直接更新 this.state 仍是放到隊列 中。

isBatchingUpdates 默認是false,也就表示setState會同步更新this.state,可是有一個函數batchedUpdates

這個函數會把isBatchingUpdates修改成true,而當React在調用事件處理函數以前就會調用這個batchedUpdates,形成的後果,就是由React控制的事件處理過程setState不會同步更新this.state。

經過上圖,咱們知道了大體流程, 要想完全瞭解它的機制,咱們解讀一下源碼。

探祕setState 源碼

// setState方法入口以下:
ReactComponent.prototype.setState = function (partialState, callback) {
  // 將setState事務放入隊列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }};

相關的幾個概念:

  • partialState,有部分state的含義,可見只是影響涉及到的state,不會傷及無辜。
  • enqueueSetState 是 state 隊列管理的入口方法,比較重要,咱們以後再接着分析。

replaceState :

replaceState: function (newState, callback) {
  this.updater.enqueueReplaceState(this, newState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'replaceState');
  }},
   replaceState中取名爲newState,有徹底替換的含義。一樣也是以隊列的形式來管理的。

enqueueSetState

enqueueSetState: function (publicInstance, partialState) {
    // 先獲取ReactComponent組件對象
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    // 若是_pendingStateQueue爲空,則建立它。能夠發現隊列是數組形式實現的
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    // 將要更新的ReactComponent放入數組中
    enqueueUpdate(internalInstance);}

其中getInternalInstanceReadyForUpdate源碼以下

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
  // 從map取出ReactComponent組件,還記得mountComponent時把ReactElement做爲key,將ReactComponent存入了map中了吧,ReactComponent是React組件的核心,包含各類狀態,數據和操做方法。而ReactElement則僅僅是一個數據類。
  var internalInstance = ReactInstanceMap.get(publicInstance);
  if (!internalInstance) {
    return null;
  }

 return internalInstance;}

enqueueUpdate源碼以下:

function enqueueUpdate(component) {
  ensureInjected();

  // 若是不是正處於建立或更新組件階段,則處理update事務
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // 若是正在建立或更新組件,則暫且先不處理update,只是將組件放在dirtyComponents數組中
  dirtyComponents.push(component);}

batchedUpdates

batchedUpdates: function (callback, a, b, c, d, e) {
  var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
  // 批處理最開始時,將isBatchingUpdates設爲true,代表正在更新
  ReactDefaultBatchingStrategy.isBatchingUpdates = true;

  // The code is written this way to avoid extra allocations
  if (alreadyBatchingUpdates) {
    callback(a, b, c, d, e);
  } else {
    // 以事務的方式處理updates,後面詳細分析transaction
    transaction.perform(callback, null, a, b, c, d, e);
  }}
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    // 事務批更新處理結束時,將isBatchingUpdates設爲了false
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

enqueueUpdate包含了React避免重複render的邏輯。

mountComponentupdateComponent 方法在執行的最開始,會調用到 batchedUpdates 進行批處理更新,此時會將isBatchingUpdates設置爲true,也就是將狀態標記爲如今正處於更新階段了。

以後React以事務的方式處理組件update,事務處理完後會調用wrapper.close() 。

TRANSACTION_WRAPPERS 中包含了RESET_BATCHED_UPDATES 這個wrapper,故最終會調用RESET_BATCHED_UPDATES.close(), 它最終會將isBatchingUpdates設置爲false

getInitialStatecomponentWillMountrendercomponentWillUpdatesetState 都不會引發 updateComponent

但在componentDidMountcomponentDidUpdate中則會。

事務

事務經過wrapper進行封裝。

clipboard.png

一個wrapper包含一對 initializeclose 方法。好比 RESET_BATCHED_UPDATES

var RESET_BATCHED_UPDATES = {
  // 初始化調用
  initialize: emptyFunction,
  // 事務執行完成,close時調用
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }};

transcation被包裝在wrapper中,好比:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

transaction 是經過transaction.perform(callback, args…)方法進入的,它會先調用註冊好的wrapper 中的initialize方法,而後執行perform方法中的callback,最後再執行close方法。

下面分析transaction.perform(callback, args…)

perform: function (method, scope, a, b, c, d, e, f) {
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      // 先運行全部wrapper中的initialize方法
      this.initializeAll(0);

      // 再執行perform方法傳入的callback
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // 最後運行wrapper中的close方法
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // 最後運行wrapper中的close方法
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },

  initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍歷全部註冊的wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
        // 調用wrapper的initialize方法
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
      } finally {
        if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

  closeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍歷全部wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
          // 調用wrapper的close方法,若是有的話
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }

更新組件: runBatchedUpdates

前面分析到enqueueUpdate中調用transaction.perform(callback, args...)後,發現,callback仍是enqueueUpdate方法啊,那豈不是死循環了?不是說好的setState會調用updateComponent,從而自動刷新View的嗎? 咱們仍是要先從transaction事務提及。

咱們的wrapper中註冊了兩個wrapper,以下:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

RESET_BATCHED_UPDATES 用來管理isBatchingUpdates狀態,咱們前面在分析setState是否當即生效時已經講解過了。

FLUSH_BATCHED_UPDATES用來幹嗎呢?

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)};
var flushBatchedUpdates = function () {
  // 循環遍歷處理完全部dirtyComponents
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // close前執行完runBatchedUpdates方法,這是關鍵
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }};

FLUSH_BATCHED_UPDATES會在一個transactionclose階段運行runBatchedUpdates,從而執行update

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  dirtyComponents.sort(mountOrderComparator);

  for (var i = 0; i < len; i++) {
    // dirtyComponents中取出一個component
    var component = dirtyComponents[i];

    // 取出dirtyComponent中的未執行的callback,下面就準備執行它了
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      if (component._currentElement.props === component._renderedComponent._currentElement) {
        namedComponent = component._renderedComponent;
      }
    }
    // 執行updateComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    // 執行dirtyComponent中以前未執行的callback
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }}

runBatchedUpdates循環遍歷dirtyComponents數組,主要幹兩件事。

  1. 首先執行performUpdateIfNecessary來刷新組件的view
  2. 執行以前阻塞的callback。

下面來看performUpdateIfNecessary

performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
      // receiveComponent會最終調用到updateComponent,從而刷新View
      ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
    }

    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      // 執行updateComponent,從而刷新View。這個流程在React生命週期中講解過
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    }
  },

最後驚喜的看到了receiveComponentupdateComponent吧。

receiveComponent最後會調用updateComponent,而updateComponent中會執行React組件存在期的生命週期方法,

componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterender, componentDidUpdate

從而完成組件更新的整套流程。

總體流程回顧:

1.enqueueSetState將state放入隊列中,並調用enqueueUpdate處理要更新的Component
2.若是組件當前正處於update事務中,則先將Component存入dirtyComponent中。不然調用batchedUpdates處理。
3.batchedUpdates發起一次transaction.perform()事務
4.開始執行事務初始化,運行,結束三個階段
5.初始化:事務初始化階段沒有註冊方法,故無方法要執行
6.運行:執行setSate時傳入的callback方法,通常不會傳callback參數
7.結束:更新isBatchingUpdates爲false,並執行FLUSH_BATCHED_UPDATES這個wrapper中的close方法
8.FLUSH_BATCHED_UPDATES在close階段,會循環遍歷全部的dirtyComponents,調用updateComponent刷新組件,並執行它的pendingCallbacks, 也就是setState中設置的callback。

看完理論, 咱們再用一個例子鞏固下.

再看一個例子:

class Example extends React.Component {
 constructor() {
   super();
   this.state = {
    val: 0
   };
}
componentDidMount() {
  this.setState({val: this.state.val + 1});
  console.log('第 1 次 log:', this.state.val);
  this.setState({val: this.state.val + 1});
  console.log('第 2 次 log:', this.state.val);

 setTimeout(() => {
  this.setState({val: this.state.val + 1});
  console.log('第 3 次 log:', this.state.val);   
  this.setState({val: this.state.val + 1});
  console.log('第 4 次 log:', this.state.val); 
 }, 0);
}
 render() {
  return null;
 }
};

前兩次在isBatchingUpdates 中,沒有更新state, 輸出兩個0。

後面兩次會同步更新, 分別輸出2, 3;

很顯然,咱們能夠將4次setState簡單規成兩類

  1. componentDidMount是一類
  2. setTimeOut中的又是一類,由於這兩次在不一樣的調用棧中執行。

咱們先看看在componentDidMount中setState的調用棧:

clipboard.png

再看看在setTimeOut中的調用棧:

clipboard.png

咱們重點看看在componentDidMount中的sw3e調用棧 :
發現了batchedUpdates方法。

原來在setState調用以前,就已經處於batchedUpdates執行的事務之中了。

batchedUpdates方法是誰調用的呢?咱們再往上追溯一層,原來是ReactMount.js中的_renderNewRootComponent方法。

也就是說,整個將React組件渲染到DOM的過程就處於一個大的事務中了。

接下來就很容易理解了: 由於在componentDidMount中調用setState時,batchingStrategyisBatchingUpdates已經被設置爲true,因此兩次setState的結果並無當即生效,而是被放進了dirtyComponents中。

這也解釋了兩次打印this.state.val都是0的緣由,由於新的state還沒被應用到組件中。

再看setTimeOut中的兩次setState,由於沒有前置的batchedUpdate調用,因此batchingStrategyisBatchingUpdates標誌位是false,也就致使了新的state立刻生效,沒有走到dirtyComponents分支。

也就是說,setTimeOut中的第一次執行,setState時,this.state.val爲1;

而setState完成後打印時this.state.val變成了2。

第二次的setState同理。

經過上面的例子,咱們就知道setState 是能夠同步更新的,可是仍是儘可能避免直接使用, 僅做了解就能夠了。

若是你非要玩一些騷操做,寫出這樣的代碼去直接去操做this.state:

this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.setState();

我只能說, 大胸弟, 你很騷。吾有舊友叼似汝,而今墳草丈許高。

結語

最後簡單重複下結論吧:

  • 不要直接去操做this.state, 這樣會形成沒必要要的性能問題和隱患。
  • 由React引起的事件處理,調用setState不會同步更新this.state,除此以外的setState調用會同步執行this.state。

我對這一套理論也不是特別熟悉, 若有紕漏, 歡迎指正 :)

最後

下面是個人公衆號: 前端e進階

有新內容會第一時間更新在這裏, 但願你們多多關注, 觀看最新內容。

clipboard.png

擴展閱讀
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...

https://medium.com/@wisecobbl...

https://zhuanlan.zhihu.com/p/...

相關文章
相關標籤/搜索