# React 源码解读(四) setState

先看这样一段代码的输出是多少

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState({
      count: me.state.count + 1
    });
    console.log("第一次执行setState", me.state.count);    // 打印 
    me.setState({
      count: me.state.count + 1
    });
    console.log("第二次执行setState", me.state.count);    // 打印  
    setTimeout(function () {
      me.setState({
        count: me.state.count + 1
      });
      // 2
      console.log('setTimeout里执行setState', me.state.count);   // 打印  
    }, 0);
    setTimeout(function () {
      batchedUpdates(() => {
        me.setState({
          count: me.state.count + 1  
        })
      })
      console.log('batchedUpdates里执行setState', me.state.count);   // 打印  
    }, 0);
    console.log('最后', me.state.count);   // 打印 
  }
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.test.bind(this)}>增加count</button>
      </div>
    );
  }
}
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
42
43

输出结果

输出结果

下面来一步一步走一下

  1. 第一个 setState 时
    me.setState({
      count: me.state.count + 1
    });
    console.log("第一次执行setState", me.state.count);    // 打印 
1
2
3
4

跟着代码调试,可以看到 requestWork 里的 isRendering=true 直接return了,所以此时的count没有+1

输出结果

  1. 第二个 setState 时

和第一次一样,也被return掉了,所以也没有执行。

  1. 第三个结果

接下来是两个setTimeout先跳过直接执行了最后的同步console也是0

  1. 第四个结果 第一个setTimeout

这里我们分别在setState的上下都输出一下count

setTimeout(function () {
      console.log('setTimeout里执行setState', me.state.count);   // 打印  1
      me.setState({
        count: me.state.count + 1
      });
      // 2
      console.log('setTimeout里执行setState', me.state.count);   // 打印  2
    }, 0);
1
2
3
4
5
6
7
8

第一个输出 1,第二个输出 2。why???

  • 因为 setTimeout 是个宏任务,在执行完同步任务后,会先做一次批处理,也就是将之前的setState合并执行,所以第一次输出1。
  • 因为 setTimeout 是个宏任务,所以会在 performWorkOnRoot 整个主流程执行完之后再执行,如图所示,此时 performWorkOnRoot 执行完所有同步任务后,已经将 isRending 置为false,所以在执行setTimeout里的 setState 时就不会再拦截return掉,所以会直接在setState后拿到count的值2。

输出结果

  • 如下图所示,isRendering 和 isBatchingUpdates 都不会进入,此时的setState就会按照同步执行,继续完成后续任务。

输出结果

  1. 第五个结果 第一个batchedUpdates
setTimeout(function () {
      batchedUpdates(() => {
        me.setState({
          count: me.state.count + 1  
        })
      })
      console.log('batchedUpdates里执行setState', me.state.count);   // 打印  
    }, 0);
1
2
3
4
5
6
7
8

跟着调试,进入到 requestWork 中,此时的 isBatchingUpdates 是true,但他不是非批处理,所以 isUnbatchingUpdates 是false,所以又被return 掉了,如图。

输出结果

接着往下,那什么时候才给它执行掉呢?接下来就是一个精妙的地方。

输出结果

使用了 try{}finally{} 来再把它执行掉!! 卧槽无情!

划重点:setState 是同步的!是同步的!是同步的!只是执行的时机不同。在生命周期中执行时通过try finally 来模拟了一个假异步!!!

在componentDidMount里会走批处理的逻辑,那么在事件中呢

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  test(){
    debugger
    this.setState({
      count: this.state.count + 1
    });
    console.log(this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    console.log(this.state.count)
    setTimeout(() => {
      // 不在流程中
      console.log(this.state.count)
      this.setState({
        count: this.state.count + 1
      });
      // 5
      console.log(this.state.count)
    }, 0);
  }
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.test.bind(this)}>增加count</button>
      </div>
    );
  }
}
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

在浏览器中调试可以看到,在点击之后会跳到 interactiveUpdates 中,然后把 isBatchingUpdates 置成了true。

输出结果

在react中事件并不是浏览器原生的事件,而是自定义的事件,而自定义的事件就要走一些逻辑了,isBatchingUpdates就是其中之一,代表着用户手动执行的更新,在执行这个事件的回调之前,会先执行interactiveUpdates,然后把 isBatchingUpdates 设置成true,这样在执行调度过程中又被return掉了,也就是说还是走了批处理的逻辑。

# 总结

setState是同步执行的,但是不会立马更新。

先等到组件已经渲染完成,setState是同步执行的,但是不会立马更新,因为他在批处理中会等待组件render才真正触发,不在批处理中的任务可能会立马更新。到底更新不更新取决于setState是否在Async的渲染过程中,因为他会进入到异步调度过程,如果setState处于我们的某个声明周期中,暂时不会BatchUpdate参与,因为组件要尽早的提前渲染。

  • 在生命周期中和事件回调中,会进行批处理,通过 isRendering 或 isBatchingUpdates return掉,最后在重新执行一次,达到批处理的目的,通过 try{}finally{} 来实现假异步。
  • 而在setTimeout中执行时,由于宏任务在整个调度流程之后才会执行,此时 isRendering 和 isBatchingUpdates 都是 false,所以会立即更新。
更新时间: 11/8/2019, 4:51:43 PM