【React】为什么setState 更新既有异步又有同步?

·  阅读 929

一、奇异之处

在React中,setState非常奇怪,初学者都会觉得它肯定是异步的,但是在某些场景下它又是同步的,这很让人疑惑。

先上代码

import React from 'react';

export default class StateDemo extends React.Component {
  state = {
    count: 0,
  };
  
  // 异步更新
  onChangeAsyncState = () => {
    this.setState({
      count: this.state.count + 1,
    });
    console.log(this.state.count);
  };

  onResetState = () => {
    this.setState({
      count: 0,
    });
    console.log(this.state.count);
  };
  
  // 同步更新
  onChangeSyncState = () => {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      });
      console.log(this.state.count);
    }, 0);
  };

  render() {
    return (
      <div>
        <p>点击下面按钮,变更count</p>
        <button onClick={this.onChangeAsyncState}>
          异步更新:{this.state.count}
        </button>
        <button onClick={this.onResetState}>重置</button>
        <button onClick={this.onChangeSyncState}>
          同步更新:{this.state.count}
        </button>
      </div>
    );
  }
}
复制代码

1.1 异步场景

这是点击异步更新按钮时,输出的结果,没有什么意外可言,点击count,然后count变成1,但是console输出的还是0,请看下面结果图
image.png

1.2 同步场景

当点击同步更新按钮时,输出的结果确有点出乎意外之外,state直接同步更新了
image.png

1.3 合并更新

其实state除了上面两种场景之外,还有合并更新这一操作,比如下面这段代码

// 合并更新
onChangeMergeState = () => {
    this.setState({
      count: this.state.count + 1,
    });
    console.log(this.state.count);
    this.setState({
      count: this.state.count + 1,
    });
    console.log(this.state.count);
};

render() {
    console.log('render', this.state.count);
    return (
      <div>
        <p>点击下面按钮,变更count</p>
        <button onClick={this.onChangeMergeState}>
          合并更新:{this.state.count}
        </button>
      </div>
    );
  }
复制代码

当点击合并按钮时,state的值并不是变成2,而是变成了1,虽然做了两次操作,但是做了一次合并,在最后执行任务队列的时候,实际上是只更新了一次state,注意console.log('render', 1)只执行了1次
image.png[

](react-ts-lchssi.stackblitz.io)

二、设计的原则

setState 最核心的作用是更新state,一旦state变更了状态 ,就会触发组件重新渲染,最后更新视图 UI。

选择什么时间更新state,这个在React RFC讨论中也是一个蛮有争议的话题,早在2017年mobxjs的作者Michel Weststrate就起了一个issue:为什么是setState异步的?为此React的核心成员Dan Abramov做了一次解释,还有包括对接下来React的新功能做了一些提前爆料

  • ** 保持内部状态一致性**:props更新是异步的,如果state同步更新了,那会引发新的问题,里面也有issue讨论
  • 为未来启用并发更新:根据事件的类型,分配不同的优先级(17之前是用ExpirationTime,之后是用Lanes),并发处理,提高渲染的性能。

在2020年5月1日React的官方人员Andrew Clark()提交了一份关于Lanes的PR,里面提到了lane的优先级和lane的任务并发模式,目前React 17已经可以体验。

上面做了演示,还有当初的一些设计原则,那么它具体的代码是怎么样的尼?

三、源码解读

React当初核心原则就是,整个UI都是一个函数,它接受一些状态(state or data),然后返回整个渲染的UI

用数学公式表达就是UI=fn(state)UI=fn(state)

用一句话来解释:state负责计算出状态变化(Reconciler),fn负责把状态渲染在ui view中(Renderer)。

所以this.setState会调用Reconciler(diff算法),用于计算state的变化,最后Renderer(渲染阶段)。

关于diff算法不是本篇的重点,重点说说this.setState的更新逻辑。

以下是setState的源码的注解,明确说了,不能保证会立即执行,因为有可能会被合并执行。

github.com/facebook/re…


/**
 * Sets a subset of the state. Always use this to mutate
 * state. You should treat `this.state` as immutable.
 *
 * There is no guarantee that `this.state` will be immediately updated, so
 * accessing `this.state` after calling this method may return the old value.
 *
 * There is no guarantee that calls to `setState` will run synchronously,
 * as they may eventually be batched together.  You can provide an optional
 * callback that will be executed when the call to setState is actually
 * completed.
 *
 * When a function is provided to setState, it will be called at some point in
 * the future (not synchronously). It will be called with the up to date
 * component arguments (state, props, context). These values can be different
 * from this.* because your function may be called after receiveProps but before
 * shouldComponentUpdate, and this new state, props, and context will not yet be
 * assigned to this.
 *
 * @param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码

this.updater这是一个ReactNoopUpdateQueue对象,从字面意义上来说就是一个更新队列。

接着继续往下执行,
github.com/facebook/re…
image.png
2107行代码决定了会去做同步执行的操作,那expirationTime什么时候变成Sync尼?

github.com/facebook/re…
image.png
上面写的很清楚,在并发模式之外都是同步执行的,那么什么情况下是并发模式尼?

在React源码中标注了,在Legacy模式下都是同步的github.com/facebook/re…
image.png
那现在是Legacy吗?在使用 Concurrent 模式这个文档里面也写了,通过这种调用方式的ReactDOM.render(, rootNode),都是Legacy。

那为什么有的代码不是同步更新尼?因为实际上真正在更新代码的过程中,还有一个参数值非常重要,叫做isBatchingUpdates,如果为true那么就进入合并更新,进而把事件放到延迟队列进行异步更新,如果为false则直接进入同步更新。

isBatchingUpdates在React生命周期内、合成事件中它会自动更新为true
github.com/facebook/re…
image.png

四、总结

前面回顾了this.setState设计原因,还有代码的执行逻辑,现在总结下,React官方一开始就是把setState设计成了异步模式,但是为了现实中的一些业务,在Legacy模式下,脱离React上下文环境的情况就出现异步模式,但是在concurrent模式下是肯定都是异步的。

下面把之前的案例修改成concurrent模式执行

// render(<App />, document.getElementById('root')); // Legacy 模式
createRoot(document.getElementById('root')).render(<App />); // concurrent模式
复制代码

输出结果已经变成了和之前异步更新的结果一模一样了。
image.png
不过concurrent模式在17版本之前,需要加上unstable_才能使用,17之后已经不需要了。

仔细研究源码之后,发现不用concurrent模式也可以把setTimeout中的同步更新变成异步更新,那就是unstable_batchedUpdates函数,这个函数也是显式的把isBatchingUpdates修改成true,从而把事件放到延迟队列进行执行,有兴趣的小伙伴可以自行尝试下。

以上的源码都是基于16.8.6这个版本,实际上在17版本之后,这块的逻辑已经基于最新的Lanes算法来进行更新了,之前是基于ExpirationTime,后面都是基于Lanes来执行,这样可以带来更高效更直观的更新逻辑。

最后

以上代码,已经上传到stackblitz.io 上了,有兴趣的可以直接点击下面链接,自己体会下
stackblitz.com/edit/react-…

引用链接

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改