其实 setState 是同步的,只不过 setState 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件,对其进行批量推迟更新的操作。而此处的“异步”并非真正的异步行为,所以大部分文章所谓的异步其实都是“异步”(意为带引号的哦~)。
setState()
并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState()
后立即读取 this.state
成为了隐患。为了消除隐患,请使用 componentDidUpdate
或者 setState
的回调函数(setState(updater, callback)
),这两种方式都可以保证在应用更新后触发。
如果不想看解析的话可以直接记答案:
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。
解析
首先说明下为什么 react 要把 setState 给“异步”化:如果 Parent
和 Child
在同一个 click 事件中都调用了 setState
,这样就可以确保 Child
不会被重新渲染两次。取而代之的是,React 会将该 state “冲洗” 到浏览器事件结束的时候,再统一地进行更新。这种机制可以在大型应用中得到很好的性能提升。而且在“异步”化的过程中,在同一周期内会对多个 setState
进行批处理。即如下所示:
相关文章参考:
Object.assign(
previousState,
{quantity: state.quantity + 1},
{quantity: state.quantity + 1},
...
)
- https://zh-hans.reactjs.org/docs/faq-state.html
- https://zh-hans.reactjs.org/docs/react-component.html#setstate
- https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973
- https://github.com/facebook/react/issues/11527#issuecomment-360199710
源码剖析
参考文章:https://juejin.im/post/5d7f219a51882501734c2921
我只看了部分,没完全读下来,感兴趣的可以去翻阅上述文章或者自己去查阅。
我本来想自己去看源码的,但是貌似没找对入口。因为目前最新的版本是v16.13.1了,估计目录结构什么的都变了。只看到部分的相关的代码,后续如果看了再进行分享。
当前源码剖析是基于 15.x 进行的剖析。
个人总结下吧:
在调用 setState 的时候,会执行 enqueueSetState,它会将要更新的 state 存到 _pendingStateQueue 队列中。然后在调用完 setState 之后,继续执行方法 enqueueUpdate。大致如下:
// ReactUpdates.js
function enqueueUpdate(component) {
// 注入默认策略,开启ReactReconcileTransaction事务
ensureInjected();
// 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState
// batchingStrategy:批量更新策略,通过事务的方式实现state的批量更新
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果batch已经开启,则将该组件保存在 dirtyComponents 中存储更新
dirtyComponents.push(component);
}
从代码中不难看出,批量更新的操作主要通过 batchingStrategy.isBatchingUpdates
来控制。如果为 false 的时候,意为不需要批量更新,那么它就会将 enqueueUpdate
作为参数传入到 batchingStrategy.batchedUpdates
方法中,在 batchedUpdates
执行更新操作。而当 batchingStrategy.isBatchingUpdates
为 true 的时候,意为着需要批量更新,那么他就会将调用 setState 的组件存入到 dirtyComponents
数组中,做存储处理,不会立即更新。
那么,batchedUpdates 什么时候被调用呢?
// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction();// 实例化事务
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 开启一次batch
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
// 启动事务, 将callback放进事务里执行
transaction.perform(callback, null, a, b, c, d, e);
}
},
};
// 说明:这里使用到了事务transaction,简单来说,transaction就是将需要执行的方法使用 wrapper 封装起来,
// 再通过事务提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,
// 执行完 perform 之后(即执行method 方法后)再执行所有的 close 方法。
// 一组 initialize 及 close 方法称为一个 wrapper。事务支持多个 wrapper 叠加,嵌套,
// 如果当前事务中引入了另一个事务B,则会在事务B完成之后再回到当前事务中执行close方法。
// ReactMount.js
_renderNewRootComponent: function(nextElement,container,shouldReuseMarkup,context) {
...
// 实例化组件
var componentInstance = instantiateReactComponent(nextElement, null);
//初始渲染是同步的,但在渲染期间发生的任何更新,在componentWillMount或componentDidMount中,将根据当前的批处理策略进行批处理
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
...
},
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
...
try {
// 处理事件
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
- 第一种情况,是在首次渲染组件时调用batchedUpdates,开启一次batch。因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如componentWillMount或者componentDidMount)调用setState. 因此, 开启一次batch就是要存储更新(放入dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState都会生效, 用户看到的始终是最新的状态。
- 第二种情况,如果在组件上绑定了事件,在绑定事件中很有可能触发setState,所以为了存储更新(dirtyComponents),需要开启批量更新策略。在回调函数被调用之前, React事件系统中的dispatchEvent函数负责事件的分发, 在dispatchEvent中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState就会生效。
所以,得出结论:
- setState 在生命周期函数和合成函数中都是异步更新。
- setState 在 setTimeout、原生事件和 async 函数中都是同步更新。