其实 setState 是同步的,只不过 setState 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件,对其进行批量推迟更新的操作。而此处的“异步”并非真正的异步行为,所以大部分文章所谓的异步其实都是“异步”(意为带引号的哦~)。


setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

如果不想看解析的话可以直接记答案:

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setStatesetState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

解析

首先说明下为什么 react 要把 setState 给“异步”化:如果 ParentChild 在同一个 click 事件中都调用了 setState ,这样就可以确保 Child 不会被重新渲染两次。取而代之的是,React 会将该 state “冲洗” 到浏览器事件结束的时候,再统一地进行更新。这种机制可以在大型应用中得到很好的性能提升。而且在“异步”化的过程中,在同一周期内会对多个 setState 进行批处理。即如下所示:

  1. Object.assign(
  2. previousState,
  3. {quantity: state.quantity + 1},
  4. {quantity: state.quantity + 1},
  5. ...
  6. )
相关文章参考:
  1. https://zh-hans.reactjs.org/docs/faq-state.html
  2. https://zh-hans.reactjs.org/docs/react-component.html#setstate
  3. https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973
  4. 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。大致如下:

  1. // ReactUpdates.js
  2. function enqueueUpdate(component) {
  3. // 注入默认策略,开启ReactReconcileTransaction事务
  4. ensureInjected();
  5. // 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState
  6. // batchingStrategy:批量更新策略,通过事务的方式实现state的批量更新
  7. if (!batchingStrategy.isBatchingUpdates) {
  8. batchingStrategy.batchedUpdates(enqueueUpdate, component);
  9. return;
  10. }
  11. // 如果batch已经开启,则将该组件保存在 dirtyComponents 中存储更新
  12. dirtyComponents.push(component);
  13. }

从代码中不难看出,批量更新的操作主要通过 batchingStrategy.isBatchingUpdates 来控制。如果为 false 的时候,意为不需要批量更新,那么它就会将 enqueueUpdate 作为参数传入到 batchingStrategy.batchedUpdates 方法中,在 batchedUpdates 执行更新操作。而当 batchingStrategy.isBatchingUpdates 为 true 的时候,意为着需要批量更新,那么他就会将调用 setState 的组件存入到 dirtyComponents 数组中,做存储处理,不会立即更新。

isBatchingUpdates 默认是 false,而 batchedUpdates 方法被调用时才会将属性 isBatchingUpdates 设置为true,表明目前处于批量更新流中。代码如下:
  1. // ReactDefaultBatchingStrategy.js
  2. var transaction = new ReactDefaultBatchingStrategyTransaction();// 实例化事务
  3. var ReactDefaultBatchingStrategy = {
  4. isBatchingUpdates: false,
  5. batchedUpdates: function(callback, a, b, c, d, e) {
  6. var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
  7. // 开启一次batch
  8. ReactDefaultBatchingStrategy.isBatchingUpdates = true;
  9. if (alreadyBatchingUpdates) {
  10. callback(a, b, c, d, e);
  11. } else {
  12. // 启动事务, 将callback放进事务里执行
  13. transaction.perform(callback, null, a, b, c, d, e);
  14. }
  15. },
  16. };
  17. // 说明:这里使用到了事务transaction,简单来说,transaction就是将需要执行的方法使用 wrapper 封装起来,
  18. // 再通过事务提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,
  19. // 执行完 perform 之后(即执行method 方法后)再执行所有的 close 方法。
  20. // 一组 initialize 及 close 方法称为一个 wrapper。事务支持多个 wrapper 叠加,嵌套,
  21. // 如果当前事务中引入了另一个事务B,则会在事务B完成之后再回到当前事务中执行close方法。
那么,batchedUpdates 什么时候被调用呢?
  1. // ReactMount.js
  2. _renderNewRootComponent: function(nextElement,container,shouldReuseMarkup,context) {
  3. ...
  4. // 实例化组件
  5. var componentInstance = instantiateReactComponent(nextElement, null);
  6. //初始渲染是同步的,但在渲染期间发生的任何更新,在componentWillMount或componentDidMount中,将根据当前的批处理策略进行批处理
  7. ReactUpdates.batchedUpdates(
  8. batchedMountComponentIntoNode,
  9. componentInstance,
  10. container,
  11. shouldReuseMarkup,
  12. context
  13. );
  14. ...
  15. },
  16. // ReactEventListener.js
  17. dispatchEvent: function (topLevelType, nativeEvent) {
  18. ...
  19. try {
  20. // 处理事件
  21. ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  22. } finally {
  23. TopLevelCallbackBookKeeping.release(bookKeeping);
  24. }
  25. }
  • 第一种情况,是在首次渲染组件时调用batchedUpdates,开启一次batch。因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如componentWillMount或者componentDidMount)调用setState. 因此, 开启一次batch就是要存储更新(放入dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState都会生效, 用户看到的始终是最新的状态。
  • 第二种情况,如果在组件上绑定了事件,在绑定事件中很有可能触发setState,所以为了存储更新(dirtyComponents),需要开启批量更新策略。在回调函数被调用之前, React事件系统中的dispatchEvent函数负责事件的分发, 在dispatchEvent中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState就会生效。

所以,得出结论:

  • setState 在生命周期函数和合成函数中都是异步更新。
  • setState 在 setTimeout、原生事件和 async 函数中都是同步更新。
以上内容有哪里说的不对的或者你有其他理解及想法的话,欢迎评论~