先给出答案:有时是同步,有时是异步。
setState在合成事件和生命周期函数里是异步的,在原生事件和setTimeout里是同步的
一、合成事件和生命周期函数里是异步的
我们可以看一个🌰:
export default class App extends React.Component {
constructor() {
super();
this.state = {
count: 0,
};
}
render() {
const { count } = this.state;
return (
<div>
<h1>{count}</h1>
<button id="add" onClick={this.btnChange}>+</button>
</div>
);
}
btnChange = () => {
this.setState({
count: this.state.count + 1,
});
console.log("this.state.count :>> ", this.state.count);
};
}
这个时候我们点击按钮,打印的state的值是0,不是最新的1,

setState的异步并不是由内部的异步代码引起的,在本身的执行过程中时同步的,但是合成事件和生命周期函数的调用顺序在更新之前,导致在内部不能直接得到更新后的值。我们可以利用setState的第二个参数callback得到最新的值,代码如下:
btnChange = () => {
this.setState({
count: this.state.count + 1,
}, () => {
//此时打印为 1
console.log("this.state.count :>> ", this.state.count);
});
//此时打印为 0
console.log("this.state.count :>> ", this.state.count);
};
需要值得注意的是在控制台中,先打印0,后打印1。
在组件的生命周期中同理,也是异步的。
二、在原生事件和setTimeout里是同步的
而在原生的DOM时间和setTimeout中,则表现为同步,我们将上面的点击事件做一下修改
btnChange = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log("this.state.count :>> ", this.state.count);
}, 0);
};
此时,打印的为更新后的count数值

DOM事件中也是一样的:
componentDidMount() {
document.querySelector("#add").addEventListener("click", () => {
this.setState({
count: this.state.count + 1,
});
console.log("this.state.count :>> ", this.state.count);
});
}
三、异步setState可能会被合并的问题
同时,异步的setState中还有一个问题,就是进行多次相同的异步setState操作时,更新前会被合并。
btnChange = () => {
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
};
答案显然意见,四次打印的都是 1

setState;需要值得注意的是,使用setState处理state的时候,并不是直接修改原来的state,而是通过setState创建一个对象,让原来的state进行处理后赋值给新的state,在本例中即count。最后将新的state和原来的state进行合并。在对象中,我们对同一个属性进行多次同样的赋值,结果肯定会被合并,并不会出现state的值被对此修改。所以上面打印的结果是四次1,而不是1,2,3,4。
那我们怎么让它不进行合并呢?
我们可以在setState中使用回调函数的形式返回一个对象:
btnChange = () => {
this.setState((state,props) => {
return {
count: state.count + 1,
};
},() => {
console.log('this.state.count :>> ', this.state.count);
});
this.setState((state,props) => {
return {
count: state.count + 1,
};
},() => {
console.log('this.state.count :>> ', this.state.count);
});
this.setState((state,props) => {
return {
count: state.count + 1,
};
},() => {
console.log('this.state.count :>> ', this.state.count);
});
this.setState((state,props) => {
return {
count: state.count + 1,
};
},() => {
console.log('this.state.count :>> ', this.state.count);
});
};
这个回调函数接受两个参数,一个是当前的state值,另一个是props,执行return {}时,count值被返回给了state中,然后再执行第二个setState,以此类推,当异步操作执行完之后,再执行setState中的callback函数,此时count的值已经是最新的4了,因此打印四次4。我们虽然看不到它从1到4打印的结果,但这个流程我们需要清楚。

而在同步的setState中,则不会进行合并
btnChange = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
});
setTimeout(() => {
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
});
setTimeout(() => {
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
});
setTimeout(() => {
this.setState({
count: this.state.count + 1,
},() => {
console.log("this.state.count :>> ", this.state.count);
});
});
};
我们可以看到打印的结果符合我们的预期:

四、结语
最后讲一下我对于React不让直接修改state的原因。记得PureComponent和shouldcomponentupdate么,常用它们进行性能优化。它的作用就是用于拦截组件渲染。比较之前的state和props跟更新后的state和props进行浅比较,从而决定是否渲染该组件。如果我们直接修改state的值,那这样的比较就没有意义了,一直是相同的,也就无从说起性能优化了。更别说如果state的值一直不变,React根本就不会重新执行render了。
由于笔者还是个前端小萌新,难免会有纰漏错误,欢迎大佬们进行指点!