揭密React setState

騰訊IMWeb團隊發表於2018-08-30
轉自IMWeb社群,作者:黃qiong,原文連結

前言

學過react的人都知道,setState在react裡是一個很重要的方法,使用它可以更新我們資料的狀態,本篇文章從簡單使用深入到setState的內部,全方位為你揭開setState的神祕面紗~

setState的使用注意事項

setState(updater, callback)這個方法是用來告訴react元件資料有更新,有可能需要重新渲染。它是非同步的,react通常會集齊一批需要更新的元件,然後一次性更新來保證渲染的效能,所以這就給我們埋了一個坑:

那就是在使用setState改變狀態之後,立刻通過this.state去拿最新的狀態往往是拿不到的。

要點一

所以第一個使用要點就是:如果你需要基於最新的state做業務的話,可以在componentDidUpdate或者setState的回撥函式裡獲取。(注:官方推薦第一種做法)

// setState回撥函式
changeTitle: function (event) {
  this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
  // Call API with the updated value
}
複製程式碼

要點二

設想有一個需求,需要在在onClick裡累加兩次,如下

  onClick = () => {
    this.setState({ index: this.state.index + 1 });
    this.setState({ index: this.state.index + 1 });
  }
複製程式碼

在react眼中,這個方法最終會變成

Object.assign(
  previousState,
  {index: state.index+ 1},
  {index: state.index+ 1},
  ...
)
複製程式碼

由於後面的資料會覆蓋前面的更改,所以最終只加了一次.所以如果是下一個state依賴前一個state的話,推薦給setState傳function

onClick = () => {
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
}
複製程式碼

以上是使用setState的兩個注意事項,接下來我們來看看setState被呼叫之後,更新元件的過程,下面是一個簡單的流程圖。

下面來逐步的解析圖裡的流程。

一、setState

ReactBaseClassses.js

ReactComponent.prototype.setState = function (partialState, callback) {
  //  將setState事務放進佇列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
複製程式碼

這裡的partialState可以傳object,也可以傳function,它會產生新的state以一種Object.assgine()的方式跟舊的state進行合併。

二、enqueueSetState

  enqueueSetState: function (publicInstance, partialState) {
     // 獲取當前元件的instance
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

     // 將要更新的state放入一個陣列裡
     var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

     //  將要更新的component instance也放在一個佇列裡
    enqueueUpdate(internalInstance);
  }
複製程式碼

這段程式碼可以得知,enqueueSetState 做了兩件事: 1、將新的state放進陣列裡 2、用enqueueUpdate來處理將要更新的例項物件

三、enqueueUpdate

ReactUpdates.js

function enqueueUpdate(component) {
  // 如果沒有處於批量建立/更新元件的階段,則處理update state事務
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果正處於批量建立/更新元件的過程,將當前的元件放在dirtyComponents陣列中
  dirtyComponents.push(component);
}
複製程式碼

由這段程式碼可以看到,當前如果正處於建立/更新元件的過程,就不會立刻去更新元件,而是先把當前的元件放在dirtyComponent裡,所以不是每一次的setState都會更新元件~。

這段程式碼就解釋了我們常常聽說的:setState是一個非同步的過程,它會集齊一批需要更新的元件然後一起更新

而batchingStrategy 又是個什麼東西呢?

四、batchingStrategy

ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy = {
  // 用於標記當前是否出於批量更新
  isBatchingUpdates: false,
  // 當呼叫這個方法時,正式開始批量更新
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 如果當前事務正在更新過程在中,則呼叫callback,既enqueueUpdate
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
    // 否則執行更新事務
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};
複製程式碼

這裡注意兩點: 1、如果當前事務正在更新過程中,則使用enqueueUpdate將當前元件放在dirtyComponent裡。 2、如果當前不在更新過程的話,則執行更新事務。

五、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物件,它暴露了一個perform的方法,用來執行anyMethod,在anyMethod執行的前,需要先執行所有wrapper的initialize方法,在執行完後,要執行所有wrapper的close方法,就辣麼簡單。

在ReactDefaultBatchingStrategy.js,tranction 的 wrapper有兩個 FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
複製程式碼

可以看到,這兩個wrapper的initialize都沒有做什麼事情,但是在callback執行完之後,RESET_BATCHED_UPDATES 的作用是將isBatchingUpdates置為false, FLUSH_BATCHED_UPDATES 的作用是執行flushBatchedUpdates,然後裡面會迴圈所有dirtyComponent,呼叫updateComponent來執行所有的生命週期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最後實現元件的更新。

以上即為setState的實現過程,最後還是用一個流程圖在做一個總結吧~

參考文件:

  1. zhuanlan.zhihu.com/p/25882602


相關文章