考虑如下代码:
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
};
打印的count是setState后的还是setState前的?
答案是setState前的
这里的setState是异步的
但是如下代码呢?
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
});
这里打印的是setState后的
这里的setState又变成同步的了
为什么呢?
其实setState在不同场景下的行为是不同的,有时候是异步的,有时候是同步的
异步场景
合成事件
考虑如下代码:
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
所有合成事件(如onClick)里面的setState是异步的
生命周期函数
考虑如下代码:
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
所有生命周期函数(如componentDidMount)里面的setState是异步的
同步场景
原生事件
考虑如下代码:
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
所有原生事件(如addEventListener)里面的setState是同步的
setTimeout、setInternal等异步调用
考虑如下代码:
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
所有异步调用(如setTimeout)里面的setState是同步的
原理
那为什么合成事件里是异步的,原生事件里就是同步的呢?它内部是怎么实现的呢?
批量更新
考虑如下代码:
class App extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is ${this.state.val}`} // 1
</div>
)
}
}
val最终是多少呢?是3吗?
其实是1
为什么呢?
这就不得不说React的批量更新机制了
React会在一次tick中把所有的setState收集到一起,如果是相同的key会被覆盖,取最后一次调用的值,上面的代码会取第三次setState的值,所以就是1
然而这和同步异步有什么关系呢?
异步原理
React把所有的setState收集到一起以后,并不会直接批量更新,而是等当前tick执行完以后,再批量更新
比如,还是上面代码:
class App extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is ${this.state.val}`} // 1
</div>
)
}
}
batchUpdates开始执行- 执行到
setState,维护一个queue,将三个setState顺序加入queue里面 - 执行
console.log(this.state.val),打印0 batchUpdates执行完毕- 依次执行
queue中的setState,遇到相同的key覆盖前一个
这就是异步的原理
然而setState内部真的是异步执行(比如setTimeout)的吗?
并不是
回到上面的示例,你可能以为第2步执行到setState时,会将setState放入setTimeout里,所以才会在第5步才执行setState
然而React并没有用到setTimeout,上面的五步是同步执行的
所以setState看起来是异步的,但是内部却是同步执行的
同步原理
现在我们知道setState异步原理是React将所有setState收集到一起以后最后批量更新
那为什么在遇到原生事件、异步调用(setTimeout)以后就变成同步的了?难道这些场景下就不批量更新了?
对喽
这些场景不在 React 的“受控上下文”中,所以就不批量更新了
比如:
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
batchUpdates开始执行- 顺序执行
setState - 执行
console.log(this.state.val),打印3 batchUpdates执行完毕
可见不批量更新以后,不但setState是同步的,而且不会合并setState,会顺序执行3个setState,所以最终console.log(this.state.val)结果是3
React 18
React 18 引入了自动批量更新,在异步场景中(如 Promise.then、fetch.then、setTimeout),也会尝试合并更新
useState
useState 的 setXxx 行为与 class 组件的 setState 一致
总结
我们要区分不同场景下的setState行为,才能更好的避免一些坑