# 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>
);
}
}
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
输出结果
下面来一步一步走一下
- 第一个 setState 时
me.setState({
count: me.state.count + 1
});
console.log("第一次执行setState", me.state.count); // 打印
2
3
4
跟着代码调试,可以看到 requestWork 里的 isRendering=true 直接return了,所以此时的count没有+1
- 第二个 setState 时
和第一次一样,也被return掉了,所以也没有执行。
- 第三个结果
接下来是两个setTimeout先跳过直接执行了最后的同步console也是0
- 第四个结果 第一个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);
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就会按照同步执行,继续完成后续任务。
- 第五个结果 第一个batchedUpdates
setTimeout(function () {
batchedUpdates(() => {
me.setState({
count: me.state.count + 1
})
})
console.log('batchedUpdates里执行setState', me.state.count); // 打印
}, 0);
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>
);
}
}
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,所以会立即更新。