vlambda博客
学习文章列表

面试题:React中setState是异步还是同步?

在学习react的过程中几乎所有学习材料都会反复强调一点setState是异步的,来看一下react官网对于setState的说明。

setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。

一个很经典的例子如下

 
   
   
 
  1. // state.count 当前为 0

  2. componentDidMount(){

  3. this.setState({count: state.count + 1});

  4. console.log(this.state.count)

  5. }

如果你熟悉react,你一定知道最后的输出结果是0,而不是1。

然而事实真的是这样吗?

我们再来看一个例子

 
   
   
 
  1. class Hello extends Component {

  2. constructor(props) {

  3. super(props);

  4. this.state = { counter: 0 };

  5. }

  6. render() {

  7. return <div onClick={this.onClick.bind(this)}>点我</div>;

  8. }


  9. componentDidMount() {

  10. //手动绑定mousedown事件

  11. ReactDom.findDOMNode(this).addEventListener(

  12. "mousedown",

  13. this.onClick.bind(this)

  14. );


  15. //延时调用onclick事件

  16. setTimeout(this.onClick.bind(this), 1000);

  17. }

  18. onClick(event) {

  19. if (event) {

  20. console.log(event.type);

  21. } else {

  22. console.log("timeout");

  23. }

  24. console.log("prev state:", this.state.counter);

  25. this.setState({

  26. counter: this.state.counter + 1

  27. });

  28. console.log("next state:", this.state.counter);

  29. }

  30. }

  31. export default Hello;

在这个组件中采用3种方法更新state

  • 在div节点中绑定onClick事件

  • 在componentDidMount中手动绑定mousedown事件

  • 在componentDidMount中使用setTimeout调用onClick

你可以猜到结果吗?输出结果是:

 
   
   
 
  1. timeout

  2. "prev state:"

  3. 0

  4. "next state:"

  5. 1

  6. mousedown

  7. "prev state:"

  8. 1

  9. "next state:"

  10. 2

  11. click

  12. "prev state:"

  13. 2

  14. "next state:"

  15. 2

结果似乎有点出人意料,三种方式只有在div上绑定的onClick事件输出了可以证明setState是异步的结果,另外两种方式显示setState似乎是同步的。

这到底是这么回事?

话不多说,直接上源码,如果你对react源码有一定了解可以接着往下看,如果没有,可以直接跳到结论(以下分析基于react15,16版本可能有出入)。

setState异步的实现

在componentWillMount中调用setState
 
   
   
 
  1. //代码位于ReactBaseClasses

  2. * @param {partialState} 设置的state参数

  3. * @param {callback} 设置state后的回调

  4. ReactComponent.prototype.setState = function(partialState, callback) {

  5. invariant(

  6. typeof partialState === 'object' ||

  7. typeof partialState === 'function' ||

  8. partialState == null,

  9. 'setState(...): takes an object of state variables to update or a ' +

  10. 'function which returns an object of state variables.',

  11. );

  12. this.updater.enqueueSetState(this, partialState);

  13. if (callback) {

  14. this.updater.enqueueCallback(this, callback, 'setState');

  15. }

  16. };

setState中调用了 enqueueSetState方法将传入的state放到一个队列中,接下来,看下 enqueueSetState的具体实现:

 
   
   
 
  1. //代码位于ReactUpdateQueue.js

  2. * @param {publicInstance} 需要重新渲染的组件实例

  3. * @param {partialState} 设置的state

  4. * @internal

  5. enqueueSetState: function(publicInstance, partialState) {

  6. //省略部分代码


  7. //从组件列表中找到并返回需渲染的组件

  8. var internalInstance = getInternalInstanceReadyForUpdate(

  9. publicInstance,

  10. 'setState',

  11. );


  12. if (!internalInstance) {

  13. return;

  14. }


  15. //state队列

  16. var queue =

  17. internalInstance._pendingStateQueue ||

  18. (internalInstance._pendingStateQueue = []);

  19. //将新的state放入队列

  20. queue.push(partialState);


  21. enqueueUpdate(internalInstance);

  22. },

enqueueSetState中先是找到需渲染组件并将新的state并入该组件的需更新的state队列中,接下来调用了 enqueueUpdate方法,接着来看:

 
   
   
 
  1. //代码位于ReactUpdateQueue.js

  2. function enqueueUpdate(internalInstance) {

  3. ReactUpdates.enqueueUpdate(internalInstance);

  4. }

  5. //代码位于ReactUpdates.js

  6. function enqueueUpdate(component) {

  7. ensureInjected();


  8. // Various parts of our code (such as ReactCompositeComponent's

  9. // _renderValidatedComponent) assume that calls to render aren't nested;

  10. // verify that that's the case. (This is called by each top-level update

  11. // function, like setState, forceUpdate, etc.; creation and

  12. // destruction of top-level components is guarded in ReactMount.)


  13. if (!batchingStrategy.isBatchingUpdates) {

  14. batchingStrategy.batchedUpdates(enqueueUpdate, component);

  15. return;

  16. }


  17. dirtyComponents.push(component);

  18. if (component._updateBatchNumber == null) {

  19. component._updateBatchNumber = updateBatchNumber + 1;

  20. }

  21. }

这段代码就是实现 setState异步更新的关键了,首先要了解的就是batchingStrategy,顾名思义就是批量更新策略,其中通过事务的方式实现state的批量更新,这里的事务和数据库中的事务的概念类似,但不完全相同,这里就不具体展开了,有时间可以具体写下,是react中十分重要也是很有意思的内容。isBatchingUpdates是该事务的一个标志,如果为true,表示react正在一个更新组件的事务流中,根据以上代码逻辑:

  • 如果没有在事务流中,调用batchedUpdates方法进入更新流程,进入流程后,会将isBatchingUpdates设置为true。

  • 否则,将需更新的组件放入dirtyComponents中,也很好理解,先将需更新的组件存起来,稍后更新。

这就解释了在componentDidMount中调用setState并不会立即更新state,因为正处于一个更新流程中,isBatchingUpdates为true,所以只会放入dirtyComponents中等待稍后更新。

事件中的调用setState

那么在事件中调用 setState又为什么也是异步的呢,react是通过合成事件实现了对于事件的绑定,在组件创建和更新的入口方法mountComponent和updateComponent中会将绑定的事件注册到document节点上,相应的回调函数通过EventPluginHub存储。当事件触发时,document上addEventListener注册的callback会被回调。从前面事件注册部分发现,此时回调函数为ReactEventListener.dispatchEvent,它是事件分发的入口方法。下面我们来看下dispatchEvent:

 
   
   
 
  1. dispatchEvent: function (topLevelType, nativeEvent) {

  2. // disable了则直接不回调相关方法

  3. if (!ReactEventListener._enabled) {

  4. return;

  5. }


  6. var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);

  7. try {

  8. // 放入

  9. ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);

  10. } finally {

  11. TopLevelCallbackBookKeeping.release(bookKeeping);

  12. }

  13. }

看到了熟悉的batchedUpdates方法,只是调用方换成了ReactUpdates,再进入ReactUpdates.batchedUpdates。

 
   
   
 
  1. function batchedUpdates(callback, a, b, c, d, e) {

  2. ensureInjected();

  3. return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);

  4. }

豁然开朗,原来在事件的处理中也是通过同样的事务完成的,当进入事件处理流程后,该事务的isBatchingUpdates为true,如果在事件中调用setState方法,也会进入dirtyComponent流程。

原生事件绑定和setTimeout中setState

在回过头来看同步的情况,原生事件绑定不会通过合成事件的方式处理,自然也不会进入更新事务的处理流程。setTimeout也一样,在setTimeout回调执行时已经完成了原更新组件流程,不会放入dirtyComponent进行异步更新,其结果自然是同步的。

顺便提一下,在更新组建时,将更新的state合并到原state是在componentWillUpdate之后,render之前,所以在componentWillUpdate之前设置的 setState可以在render中拿到最新值。

总结

1.在组件生命周期中或者react事件绑定中,setState是通过异步更新的。2.在延时的回调或者原生事件绑定的回调中调用setState不一定是异步的。

这个结果并不说明setState异步执行的说法是错误的,更加准确的说法应该是setState不能保证同步执行。

Dan Abramov也多次提到今后会将setState改造为异步的,从js conf中提到的suspend新特新也印证了这一点