React 源碼剖析系列 - 解密 setState

this.setState() 方法應該是每一位使用 React 的同窗最早熟悉的 API。然而,你真的瞭解 setState 麼?先看看下面這個小問題,你可否正確回答。javascript

引子

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;
  }
};

問上述代碼中 4 次 console.log 打印出來的 val 分別是多少?java

不賣關子,先揭曉答案,4 次 log 的值分別是:0、0、二、3。react

若結果和你心中的答案不徹底相同,那下面的內容你可能會感興趣。git

一樣的 setState 調用,爲什麼表現和結果卻截然不同呢?讓咱們先看看 setState 到底幹了什麼。github

setState 幹了什麼

setState 簡化調用棧

上面這個流程圖是一個簡化的 setState 調用棧,注意其中核心的狀態判斷,在源碼(ReactUpdates.js)數組

function enqueueUpdate(component) {
  // ...

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

  dirtyComponents.push(component);
}

isBatchingUpdates 爲 true,則把當前組件(即調用了 setState 的組件)放入 dirtyComponents 數組中;不然 batchUpdate 全部隊列中的更新。先無論這個 batchingStrategy,看到這裏你們應該已經大概猜出來了,文章一開始的例子中 4 次 setState 調用表現之因此不一樣,這裏邏輯判斷起了關鍵做用。app

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

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

熟悉 MySQL 的同窗看到 Transaction 是否會心一笑?然而在 React 中 Transaction 的原理和行爲和 MySQL 中並不徹底相同,讓咱們從源碼開始一步步開始瞭解。ui

在 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>
 */

簡單地說,一個所謂的 Transaction 就是將須要執行的 method 使用 wrapper 封裝起來,再經過 Transaction 提供的 perform 方法執行。而在 perform 以前,先執行全部 wrapper 中的 initialize 方法;perform 完成以後(即 method 執行後)再執行全部的 close 方法。一組 initialize 及 close 方法稱爲一個 wrapper,從上面的示例圖中能夠看出 Transaction 支持多個 wrapper 疊加。

具體到實現上,React 中的 Transaction 提供了一個 Mixin 方便其它模塊實現本身須要的事務。而要使用 Transaction 的模塊,除了須要把 Transaction 的 Mixin 混入本身的事務實現中外,還須要額外實現一個抽象的 getTransactionWrappers 接口。這個接口是 Transaction 用來獲取全部須要封裝的前置方法(initialize)和收尾方法(close)的,所以它須要返回一個數組的對象,每一個對象分別有 key 爲 initialize 和 close 的方法。

下面是一個簡單使用 Transaction 的例子

var Transaction = require('./Transaction');

// 咱們本身定義的 Transaction
var MyTransaction = function() {
  // do sth.
};

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();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform

固然在實際代碼中 React 還作了異常處理等工做,這裏不詳細展開。有興趣的同窗能夠參考源碼中 Transaction 實現。

說了這麼多 Transaction,它究竟是怎麼致使上文所述 setState 的各類不一樣表現的呢?

解密 setState

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

componentDidMout 中 setState 的調用棧

componentDidMout 中 setState 的調用棧

setTimeout 中 setState 的調用棧

setTimeout 中 setState 的調用棧

很明顯,在 componentDidMount 中直接調用的兩次 setState,其調用棧更加複雜;而 setTimeout 中調用的兩次 setState,調用棧則簡單不少。讓咱們重點看看第一類 setState 的調用棧,有沒有發現什麼熟悉的身影?沒錯,就是 batchedUpdates 方法,原來早在 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 同理。

擴展閱讀

在上文介紹 Transaction 時也提到了其在 React 源碼中的多處應用,想必調試過 React 源碼的同窗應該能常常見到它的身影,像 initialize、perform、close、closeAll、notifyAll 等方法出如今調用棧中,都說明當前處於一個 Transaction 中。

既然 Transaction 這麼有用,咱們本身的代碼中能使用 Transaction 嗎?很惋惜,答案是不能。不過針對文章一開始例子中 setTimeout 裏的兩次 setState 致使兩次 render 的狀況,React 偷偷給咱們暴露了一個 batchedUpdates 方法,方便咱們調用。

import ReactDom, { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  this.setState(val: this.state.val + 1);
  this.setState(val: this.state.val + 1);
});

固然由於這個不是公開的 API,後續存在廢棄的風險,你們在業務系統裏慎用喲!

註釋

  1. test-react 文中測試代碼已放在 Github 上,須要本身實驗探索的同窗能夠 clone 下來本身斷點調試。

  2. 爲了不引入更多的概念,上文中所說到的 batchingStrategy 均指 ReactDefaultBatchingStrategy,該 strategy 在 React 初始化時由 ReactDefaultInjection 注入到 ReactUpdates 中做爲默認的 strategy。在 server 渲染時,則會注入不一樣的 strategy,有興趣的同窗請自行探索。

相關文章
相關標籤/搜索