在React官方文档中有这么一句话React does not guarantee that the state changes are applied immediately。在我最开始使用React的时候,我只是简单的把这句话当做React这个框架的约束,但是随着使用的深入,setState这个函数也让我觉得越来越神秘。在这篇文章中,我将通过反思自己在使用react中遇到的关于setState的一些问题,深入react源码,分析setState这个函数。

以下代码全部基于React15(React16代码太复杂了看不懂哇- -)。

setState不一定是同步的

在React官方文档中有这么一句话state-updates-may-be-asynchronous

下面这两个很经典也是新人很容易糊涂的场景就是由上面这句模棱两可的话带来的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Demo extends Component {
    state = {
        count: 1
    }

    onClickHandler = ()  => {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count); // console.log 结果 1
        this.setState({count: this.state.count + 1});
        console.log(this.state.count); // console.log 结果 1
    }

    render() {
        const { count } = this.state;
        return (
            <button onClick={this.onClickHandler}>{count}</button>
        );
    }
}

class SetTimeoutDemo extends Component {
    state = {
        count: 1
    }

    onClickSetTimeoutHandler = () => {
        setTimeout(() => {
            this.setState({count: this.state.count + 1});
            console.log(this.state.count); // console.log 结果 2
            this.setState({count: this.state.count + 1});
            console.log(this.state.count); // console.log 结果 3
        }, 0);
    }

    render() {
        const { count } = this.state;
        return (
            <button onClick={this.onClickSetTimeoutHandler}>{count}</button>
        );
    }
}

stackBlitz的demo在这 ClickerSetTimeoutClicker

上面的两个结果非常令人疑惑,在我刚刚接触React的时候,我并不是特别理解为什么这种写法会产生这样的差异,只是简单的相信这是React的一种Magic。

setState的内部实现

但是所有的Magic其实都有踪可循,经过一番调查,我大致理清楚了setState内部实现的调用关系。

setState的内部调用栈如上图所示,略去一些细枝末节的代码之后,简化为如下的流程图。

在这个流程图中,有几个非常重要的地方需要关注

  1. 所有的 setState 时更新的state都以partial state的形式进入一个队列中,等待在batchUpdate中进行一次更新
  2. batch update有一个 isBatchingUpdate 的锁,当正在进行batching update时,无法再次触发batching update,当前component被push到 dirtyComponent 数组中等待后续更新

以事务(transaction)的方式进行更新

在batch update中,React使用事务机制进行更新,事务机制的运行原理如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
 *
*/

事务会在创造时注入多个wrapper,每个wrapper是一个有着 initializeclose 两个函数的对象。当执行 perform(anyMethod) 的时候,调用顺序依次为

1
2
3
/**
  wrapper1.initialize -> wrapper2.initialize -> anymethod -> wrapper1.close -> wrapper2.close
*/

batch update中的事务

React实现了两个Wrapper用作batch update,在接入事务后,batch update的流程如下

React在一次事务中完成 batch update锁的打开和关闭,来保证batch update的进行。

再回头看看我们之前的问题

回到我们之前的问题

在解释过 setState 内部的工作原理之后,其实对于上面这种奇怪的输出已经不难理解了。React在事件触发时就已经处在了一个大的事务之中isBatchingUpdate 被置成了 true ,随后的setState在调用时会进入 dirtyComponent 队列,在下一次batch update中进行更新。所以在下一次batch update之前, this.state 都不会得到更新。所以事实上调用结果如下。

1
2
3
4
5
//this.state.count = 1
this.setState({count: this.state.count + 1});
// 等于this.setState({count: 2})
this.setState({count: this.state.count + 1});
// 等于this.setState({count: 2})

而如果 setState 函数进行了 setTimeout 的包裹,由于EventLoop的特点,会保证 setState 一定是在前一条message之后,也就是上一次batch update完之后进行执行, isBatchingUpdatefalse=,此时的 =setState 会直接触发一次完整的batch update,保证 this.state 被同步更新。而下一次再进行 setTimeout 包裹的 setState 操作原理同上。

因为相同的原因,在组件生命周期中调用 setState 方法也会和事件触发类似, setState 并不会跟预期中的一样进行同步更新。

还有什么方式可以同步更新state?

在上面的例子中,我们提到,在使用React封装的事件时会进入一个事务,使得 isBatchingUpdatetrue 。 而当我们使用原生的事件机制时(比如 addEventListener ),由于缺少了React的封装,会使得 setState 直接触发 batch update更新,从而同步更新state。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RawDemo extends Component {
  constructor() {
    super();
    this.state = {
      count: 1
    };
  }

  componentDidMount() {
   document.querySelector('#foo').addEventListener('click', () => {
     this.setState({count: this.state.count + 1});
     console.log(this.state.count);
     this.setState({count: this.state.count + 1});
     console.log(this.state.count);
     })
  }

  render() {
    return (
      <button id="foo">{this.state.count}</button>
    );
  }
}

DEMO在这

总结

  • React中的batch update使用事务进行完成
  • React通过 isBatchingUpdate 来控制是否更新组件,当 isBatchingUpdatetrue 时,组件会被推入 dirtyComponent 数组中而不会即时更新
  • 普通情况下之所以 setState 表现为非同步,原因是在React封装的事件绑定(或者在生命周期)中调用 setState 处于一个大的事务中, isBatchingUpdate 已经被置为 true
  • 除了通过 setTimout , 还可以通过原生的事件绑定机制来同步更新state(并不推荐使用)。

参考资料

setState:这个API设计到底怎么样

React - setState源码分析(小白可读)