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 方法不会被屡次调用。

相关文章
相关标签/搜索