React組件的DidMount事件裏的setState事件

參考原文:react

  1. React 源碼剖析系列 - 解密 setStateapp

  2. setState 以後發生了什麼 —— 淺談 React 中的 Transactiondom

沒法屢次setState

React組件的componentDidMount事件裏使用setState方法,會有一些有趣的事情:函數

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

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};
複製代碼

運行這段代碼,咱們能夠看到屏幕裏打印的是0、0、二、3。ui

爲何setState不成功

這好像跟咱們想象中的不大同樣,咱們先看下setState流程圖,看看這個方法裏發生了什麼事情this

  咱們能夠看到,若是處於批量更新階段內,就會把全部更改的操做存入pending隊列,當咱們已經完成批量更新收集階段,咱們讀取pengding隊列裏的操做,一次性處理並更新state。那麼根據上面的執行結果,咱們大概能夠猜到,前面兩個setState操做應該是恰好處於批量更新階段,這兩個操做都被收集到隊列裏,即state在這個階段裏暫時不會被更改,因此仍是保留原始值0。spa

  當setTiemout的時候,跳出了當前執行的任務隊列,估計相應也跳出了批量更新階段,因此致使如今的操做會當即體如今state(此時通過上面的更改,state已經變成了1)裏。因此後面兩個操做會致使state值陸續變成二、3。若是用任務隊列的方式這麼理解,好像是說得通,那麼咱們關心的是爲何componentDidMount事件裏就處於batch update了,也就是batch update實際上是什麼東西?3d

查看React源碼裏,setState裏源碼對應下面這段:code

function enqueueUpdate(component) {
  // ...

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}
複製代碼

也就是由batchingStrategy的isBatchingUpdates屬性來決定當前是否處於批量更新階段,而後再由batchingStrategy來執行批量更新。component

那麼batchingStrategy是什麼?其實它只是一個簡單的對象,定義了一個 isBatchingUpdates 的布爾值,和一個 batchedUpdates 方法。下面是一段簡化的定義代碼:

var batchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    // ...
    batchingStrategy.isBatchingUpdates = true;
    
    transaction.perform(callback, null, a, b, c, d, e);
  }
};
複製代碼

注意 batchingStrategy 中的 batchedUpdates 方法中,有一個 transaction.perform 調用。這就引出了本文要介紹的核心概念 —— Transaction(事務)。

Transaction

在 Transaction 的源碼中有一幅特別的 ASCII 圖,形象的解釋了 Transaction 的做用。

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

咱們能夠看到,其實在內部是經過將須要執行的method使用wrapper封裝起來,再託管給Transaction提供的perform方法執行,由Transaction統一來初始化和關閉每一個wrapper。

解密 setState

那麼 Transaction 跟 setState 的不一樣表現有什麼關係呢?首先咱們把 4 次 setState 簡單歸類,前兩次屬於一類,由於他們在同一次調用棧中執行;setTimeout 中的兩次 setState 屬於另外一類,緣由同上。讓咱們看看componentDidMout 中 setState 調用棧:

而setTimeout 中 setState 的調用棧以下:

咱們能夠看到,裏邊的setState是包裹在batchedUpdates的Transaction裏執行的。那此次 batchedUpdate 方法,又是誰調用的呢?讓咱們往前再追溯一層,原來是ReactMount.js中的_renderNewRootComponent方法。也就是說,整個將React組件渲染到DOM中的過程就處於一個大的Transaction中。

接下來的解釋就瓜熟蒂落了,由於在componentDidMount中調用setState時,batchingStrategy的isBatchingUpdates已經被設爲true,因此兩次setState的結果並無當即生效,而是被放進了 dirtyComponents 中。這也解釋了兩次打印this.state.val都是 0 的緣由,新的state尚未被應用到組件中。

再反觀setTimeout中的兩次setState,由於沒有前置的batchedUpdate調用,因此batchingStrategy的isBatchingUpdates標誌位是false,也就致使了新的state立刻生效,沒有走到dirtyComponents分支。也就是,setTimeout中第一次setState時,this.state.val爲 1,而setState 完成後打印時this.state.val變成了 2。第二次setState同理。

爲何點擊事件屢次setState失敗

咱們再看看下面的例子

var Example = React.createClass({
  getInitialState: function() {
    return {
      clicked: 0
    };
  },

  handleClick: function() {
    this.setState({clicked: this.state.clicked + 1});
    this.setState({clicked: this.state.clicked + 1});
	console.log(this.state.clicked)
  },

  render: function() {
    return <button onClick={this.handleClick}>{this.state.clicked}</button>;
  }
});
複製代碼

執行以後,咱們能夠看到,其實只調用了一遍setState,而且this.state.clicked等於0

詳細流程說明

上面的流程圖中只保留了部分核心的過程,看到這裏你們應該明白了,全部的 batchUpdate 功能都是經過託管給transaction實現的。this.setState 調用後,新的 state 並無立刻生效,而是經過 ReactUpdates.batchedUpdate 方法存入臨時隊列中。當外層的transaction 完成後,才調用ReactUpdates.flushBatchedUpdates 方法將全部的臨時 state merge 並計算出最新的 props 及 state。

縱觀 React 源碼,使用 Transaction 之處很是之多,React 源碼註釋中也列舉了不少可使用 Transaction 的地方,好比

  • 在一次 DOM reconciliation(調和,即 state 改變致使 Virtual DOM 改變,計算真實 DOM 該如何改變的過程)的先後,保證 input 中選中的文字範圍(range)不發生變化
  • 當 DOM 節點發生從新排列時禁用事件,以確保不會觸發多餘的 blur/focus 事件。同時能夠確保 DOM 重拍完成後事件系統恢復啓用狀態。
  • 當 worker thread 的 DOM reconciliation 計算完成後,由 main thread 來更新整個 UI
  • 在渲染完新的內容後調用全部 componentDidUpdate 的回調 等等

值得一提的是,React 還將 batchUpdate 方法暴露了出來:

var batchedUpdates = require('react-dom').unstable_batchedUpdates;
複製代碼

當你須要在一些非 DOM 事件回調的函數中屢次調用 setState 等方法時,能夠將你的邏輯封裝後調用 batchedUpdates 執行,以此保證 render 方法不會被屢次調用。

相關文章
相關標籤/搜索