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,有兴趣的同窗请自行探索。

相关文章
相关标签/搜索