其实 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.jsfunction 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.jsvar transaction = new ReactDefaultBatchingStrategyTransaction();// 实例化事务var ReactDefaultBatchingStrategy = {isBatchingUpdates: false,batchedUpdates: function(callback, a, b, c, d, e) {var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;// 开启一次batchReactDefaultBatchingStrategy.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.jsdispatchEvent: 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 函数中都是同步更新。
