前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React中的setState的同步异步与合并

React中的setState的同步异步与合并

作者头像
Qwe7
发布2022-06-09 08:35:43
1.5K0
发布2022-06-09 08:35:43
举报
文章被收录于专栏:网络收集

原理图

原理可以用这张图来描述,即在react中,setState通过一个队列机制实现state的更新。当执行setState时,会把需要更新的state合并后放入状态队列,而不会立刻更新this.state,当进入组件可更新状态时,这个队列机制就会高效的批量的更新state。

partialState:setState传入的第一个参数,对象或函数

_pendingStateQueue:当前组件等待执行更新的state队列

isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用

dirtyComponent:当前所有处于待更新状态的组件队列

transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close

FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

执行过程

对照上面流程图的文字说明,大概可分为以下几步:

1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。

2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。

3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。

4.调用事务的waper方法,遍历待更新组件队列依次执行更新。

5.执行生命周期componentWillReceiveProps。

6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。

7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。

8.执行生命周期componentWillUpdate。

9.执行真正的更新,render。

10.执行生命周期componentDidUpdate。

总结

1.钩子函数和合成事件中:

在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。

按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

也就是前言中的那题的来源

2.异步函数和原生事件中

由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

3.partialState合并机制

我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的state。

代码语言:javascript
复制

  _processPendingState: function (props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = _assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }

    return nextState;
  },

我们只需要关注下面这段代码:

_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);

1

如果传入的是对象,很明显会被合并成一次:

代码语言:javascript
复制
Object.assign(
  nextState,
  {index: state.index+ 1},
  {index: state.index+ 1}
)

如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。

4.componentDidMount调用setstate

在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。

以上是官方文档的说明,不推荐直接在componentDidMount直接调用setState,由上面的分析:componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

当然在componentDidMount我们可以调用接口,再回调中去修改state,这是正确的做法。

当state初始值依赖dom属性时,在componentDidMount中setState是无法避免的。(或者可以使用原生事件监听)

5.componentWillUpdate componentDidUpdate这两个生命周期中不能调用setState。

由上面的流程图很容易发现,在它们里面调用setState会造成死循环,导致程序崩溃。

最后再看一道常见面试题

代码语言:javascript
复制
class Example extends React.Component{
    constructor(){
    super(...arguments)
        this.state = {
            count: 0
        };
    }
    componentDidMount(){
       // a
      this.setState({count: this.state.count + 1});
      console.log('1:' + this.state.count)
      // b
      this.setState({count: this.state.count + 1});
      console.log('2:' + this.state.count)
      setTimeout(() => {
        // c
        this.setState({count: this.state.count + 1});
        console.log('3:' + this.state.count)
      }, 0)
      // d
      this.setState(preState => ({ count: preState.count + 1 }), () => {
        console.log('4:' + this.state.count)
      })
      console.log('5:' + this.state.count)
      // e
      this.setState(preState => ({ count: preState.count + 1 }))
      console.log('6:' + this.state.count)
    }
}

思考一下,你的答案是什么???

你的答案是否正确?你又是否理解为什么会出现上面的答案?接下来我们就来仔细分析一下。

setState(updater, [callback])

setState 可以接受两个参数,第一个参数可以是一个对象或者是一个函数,都是用来更新 state。第二个参数是一个回调函数(相当于Vue中的$NextTick ),我们可以在这里拿到更新的 state。

在上面的代码中,【a,b,c】的 setState 的第一个参数都是一个对象,【e,f】的 setState 的第一个参数都是函数。

首先,我们先说说执行顺序的问题。

【1,2,5,6】最先打印,【4】在中间,最后打印【3】。因为【1,2,5,6】是同步任务,【4】是回调,相当于 NextTick 微任务,会在同步任务之后执行,最后的【3】是宏任务,最后执行。

接下来说说打印的值的问题。

在【1,2,5,6】下面打印的 state 都是0,说明这里是异步的,没有获取到即时更新的值;

在【4】里面为什么打印出3呢?

首先在【a,b】两次 setState 时,都是直接获取的 this.state.count 的值,我们要明白,这里的这个值有“异步”的性质(这里的“异步”我们后面还会讲到),异步就意味着这里不会拿到能即时更新的值,那每次 setState 时,拿到的 this.state.count 都是0。

在【d,e】两个 setState 时,它的参数是函数,这个函数接收的第一个参数 preState (旧的 state ),在这里是“同步”的,虽有能拿到即时更新的值,那么经过【a,b】两次 setState (这里类似于被合并),这里即时的 count 还是1。因为上面我们说过的执行顺序的关系,再经过【d,e】两次 setState ,所以 count 变成了3。

那么在【3】中打印出4又是为什么?你不是说了在 this.state.count 中拿到的值是“异步”的吗,不是应该拿到0吗,怎么会打印出4呢?

代码语言:javascript
复制
method() {
    isBatchingUpdate = true;
    // 你需要执行的一些代码
    // ...
    isBatchingUpdate = false
}

那么在上面的那个面试题中,在 setTimeout 执行的时候 isBatchingUpdate 是 false ,没有命中 batchUpdate 机制,所有同步更新,这里的 this.state.count 已经是 3 了,所有在【3】中打印的就是 4。

代码语言:javascript
复制
componentDidMount(){
    isBatchingUpdate = true
    setTimeout(() => {
      // c
      // 由于执行顺序的原因,在这里 isBatchingUpdate 已经是 false 了,所以同步更新
      this.setState({count: this.state.count + 1});
      console.log('3:' + this.state.count)
    }, 0)
    isBatchingUpdate = false
}

以上是这个面试题的问题。还有一些 react 中自定义的 DOM 事件,同样是异步代码,也遵循这个 batchUpdata 机制,明白了这其中的原理,啥面试题都难不住我们。

那么接下来我们做下总结:

this.state是否异步,关键是看是否命中 batchUpdata 机制,命中就异步,未命中就同步。

setState 中的 preState 参数,总是能拿到即时更新(同步)的值。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档