先给出答案:有时是同步,有时是异步。
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
了。
由于笔者还是个前端小萌新,难免会有纰漏错误,欢迎大佬们进行指点!