面试官:"React中的setState是同步的还是异步的?"
我:"在react合成事件和钩子函数中是异步的,在原生事件和setTimeout中都是同步的,哼~小样"
面试官:"为什么setState有些时候是同步,有些时候是异步的?"
我: "......"
我们都知道,setState在react里是一个很重要的方法,使用它可以更新组件状态,但是为什么相同的setState方法在不同地方使用会呈现出不同的表现形式呢?今天,我们从源码的角度带大家探究setState是如何完成的以及为什么会有同步和异步的区别。
setState(updater, callback)方法执行后,react通常会集齐一批需要更新的组件,然后批量更新来保证渲染性能,所以我们在使用setState改变状态后,通常没办法立刻通过this.state拿到最新的状态。
如果你需要拿到最新的状态,可以在componentDidUpdate或者setState的callback里获取。
合成事件中的setState
合成事件
首先的了解什么是合成事件,react为了解决跨平台兼容性的问题,封装了一套自己的事件机制,代理了浏览器的原生事件,在JSX中常见的onClick、onChange这些都是合成事件。
class App extends React.Component {
state = { count: 0 };
onClick = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return <button onClick={this.onClick} />;
}
}
合成事件中setState的调用栈分析
合成事件中的setState比较常见,onClick事件里去更新this.state.count的值,整个合成事件到state更新我们可以通过断点看到整个调用栈,大概流程如下:
从上面可以看到从我们点击到执行原生的click事件之间react做了大量的合成事件处理逻辑,到callCallback这里我们代码只是走了合成事件的处理流程,从setState到requestWork是调用this.setState后的相关逻辑。
我们这里看一下requestWork部分的代码:
function requestWork(root, expirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpiration(expirationTime);
}
}
在 requestWork 中有三个if分支,三个分支中有两个方法 performWorkOnRoot 和 performSyncWork ,就是我们默认的update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdates 和 isUnbatchingUpdates 两个初始值都为 false ,但是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:
function interactiveUpdates$1(fn, a, b) {
if (isBatchingInteractiveUpdates) {
return fn(a, b);
}
// If there are any pending interactive updates, synchronously flush them.
// This needs to happen before we read any handlers, because the effect of
// the previous event may influence which handlers are called during
// this event.
if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
// Synchronously flush pending interactive updates.
performWork(lowestPendingInteractiveExpirationTime, false, null);
lowestPendingInteractiveExpirationTime = NoWork;
}
var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true; // 把requestWork中的isBatchingUpdates标识改为true
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}
在这里isBatchingUpdates状态被置为true, 而isUnbatchingUpdates为 false, 所以在requestWork方法中直接被return了,return之后回到interactiveUpdates$1方法中,上面说到,从dispatchEvent到requestWork整个都是在interactiveUpdates$1方法的try代码块中,也就是这个fn(a, b),所以在你在合成事件中setState后立即去console.log时,此时state并没有完成更新,所以console.log是无法拿到最新的state的值。这就导致了state更新的“异步”。
但是当你的try代码块执行完的时候(也就是你的increment合成事件),这个时候会去执行 finally 里的代码,在 finally 中执行了 performSyncWork 方法,这个时候才会去更新你的 state 并且渲染到UI上。
生命周期钩子中的setState
class App extends React.Component {
state = { count: 0 };
componentDidMount(){
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
}
render() {
return <button onClick={this.onClick} />;
}
}
其实生命周期钩子中的setState和合成事件一样,当 componentDidmount 执行的时候,react内部并没有更新,执行完componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmount 中 setState 后,console.log输出的结果还是更新前的值。
原生事件中的setState
class App extends React.Component {
state = { count: 0 };
addCount = () => {
this.setState({ count: this.state.count + 1 })
}
componentDidMount(){
document.body.addEventListener('click', this.addCount, false);
}
render() {
return <div>{this.state.count}</div>;
}
}
原生事件是指JavaScript自带的事件监听,通过addEventListener或document.querySelector().onclick这种习形式绑定的事件,与react的合成事件相区别。
在原生事件触发后并不会走合成事件那一大堆逻辑,直接触发handle方法后执行setState,setState后代码执行到前面合成事件里说过的requestWork方法,这里我们再回顾一遍代码:
function requestWork(root, expirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpiration(expirationTime);
}
}
在原生事件中,requestWork方法isRendering和isBatchingUpdates状态都为false, 所以这里不会像合成事件触发时在这里被return,而是会直接走进expirationTime === Sync分支,执行performSyncWork方法去处理更新State。所以在原生事件中,你可以在setState后同步拿到最新的state值。
setTimeout中的setState
那在setTimeout中执行的setState又是如何可以同步拿到最新的值的呢?setTimeout可以在合成事件里执行,也可以在原生事件、生命周期钩子里执行,它是如何保证能够同步获取最新state的呢?
基于EventLoop模型,我们知道setTimeout属于宏任务。那么在合成事件中,try块代码里执行到setTimeout时会将它放入宏任务队列,而先执行finally代码块的更新部分逻辑,当finally代码块执行完毕后,isBatchingUpdates状态也会被置为false,这就导致最后在执行setTimeout部分setState逻辑时,在requestWork方法中会走跟原生事件相同的expirationTime === Sync分支,所以setTimeout里面setState后可以拿到最新的state了。
总结
通过上面的分析我们可以知道,在react合成事件和生命周期方法中,setState是“异步”的,在原生事件和setTimeout中,setState是同步的。
setState的异步并不是说其内部由异步代码实现,其实本身逻辑代码都是同步的,只不过在合成事件及生命周期方法中获取state的部分代码在state更新之前被调用,导致没办法立刻拿到最新的state,从而形成所谓的“异步”。
以上就是这篇文章的全部内容了,主要结合部分源码介绍了不同场景下setState的执行与更新逻辑。如果对你有帮助,不要忘了给我点赞转发收藏三连,如果有任何想法欢迎在下方留言。
我是小柒,我们下期见。