本文将以一道面试题“react中setState是同步的还是异步?”进行解读。
补充说明:setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
setState表现为异步的场景如下:
1.合成事件中的setState
react自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
class SetStatus extends Component {
state = { val: 0 }
increment = () => {
debugger
this.setState({val: this.state.val +1})
console.log('---------'. val) // 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
只有当increment函数执行完以后,才会更新state,执行performSyncWorkOnRoot
2.生命周期函数中的setState
class StudySetState extends Component {
state = {val: 0}
componentDidMount() {
debugger
this.setState({val: this.state.val + 1})
console.log('-----', this.state.val) // 0
}
render() {
return (
<div>
{`Counter is ${this.state.val}`}
</div>
)
}
}
原理:其实还是和合成事件一样,当 componentDidmount 执行的时候,react内部并没有更新,执行完componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmount 中 setState 完去console.log拿的结果还是更新前的值。
源码真相:
if ( finishedWork.mode & ProfileMode) {
try {
startLayoutEffectTimer();
instance.componentDidMount(); // 执行完componentDidMount()
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidMount();
}
var updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
{
if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
if (instance.props !== finishedWork.memoizedProps) {
error('Expected %s props to match memoized props before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
}
if (instance.state !== finishedWork.memoizedState) {
error('Expected %s state to match memoized state before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
}
}
} // We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
commitUpdateQueue(finishedWork, updateQueue, instance); // 更新props和state
}
setState表现为同步的场景如下:
1.原生事件的setState
class StudySetState 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>
)
}
}
原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件。
原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到 requestWork ,在requestWork里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。
2.setTimeout中的setState
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 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于事件循环机制,setTimeout中里去setState总能拿到最新的state值。
不同react版本下setState的表现
在react18之前
- 在组件生命周期或React合成事件中,setState是异步
- 在setTimeout或者原生dom事件以及Promise这类不是由React进行发起的事件中,setState是同步
在react18之后
- 在React18之后,默认所有的操作都被放到了批处理中(异步处理)
- 如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作
class StudySetState extends Component {
state = { val: 0}
handleClick = () => {
flushSync(() => {
this.setState({val: 2})
})
console.log('-------', this.state.val) // 2
}
render() {
return (
<button onClick={this.handleClick}>
{`Counter is: ${this.state.val}`}
</button>
)
}
}