React———setState到底是同步还是异步(上)

599 阅读4分钟

浅谈setState

经常在项目的数据流乱作一团的时候,层层排查到最后,始作俑者也往往是 setState——工作机制太复杂,前期官方文档不太容易理解。于是结合诸位大佬的文章,做了一下个人总结。

在官方文档 React.Component章节中的描述:

不同于生命周期方法(React 主动调用),以下方法是你可以在组件中调用的方法。
只有两个方法:setState() 和 forceUpdate()。

关于setState

state 到底是同步还是异步的?

想必各位看官都有自己的理解, 在正式切入之前,我们先来看下面这段代码,这是一道变体繁多的面试题,在面试中考察频率非常高。

import React from "react";

import "./styles.css";

export default class App extends React.Component{

  state = {
    count: 0
  }
  //点击+1
  add = () => {
    console.log('add setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    console.log('add setState后的count', this.state.count)
  }
  //点击+2
  addDouble = () => {
    console.log('addDouble setState前的count', this.state.count)
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
    console.log('addDouble setState后的count', this.state.count)
  }

  reduce = () => {
    setTimeout(() => {
      console.log('reduce setState前的count', this.state.count)
      this.setState({
        count: this.state.count - 1
      });
      console.log('reduce setState后的count', this.state.count)
    },0);
  }

  render(){
    return <div>
      <button onClick={this.add}>点我增加</button>
      <button onClick={this.addDouble}>点我增加2</button>
      <button onClick={this.reduce}>点我减少</button>
    </div>
  }
}

此时有个问题,若从左到右依次点击每个按钮,控制台的输出会是什么样的?读到这里,建议你先暂停 1 分钟在脑子里跑一下代码,看看和下图实际运行出来的结果是否有出入

 控制台打印:add setState前的count 0
 
           add setState后的count 0
           
           addDouble setState前的count 1
           
           addDouble setState后的count 1
           
           reduce setState前的count 2
           
           reduce setState后的count 1

在初学的时候会听到过一个结论:setState 是一个异步的方法,这意味着当我们执行完 setState 后,state 本身并不会立刻发生改变。 因此紧跟在 setState 后面输出的 state 值,仍然会维持在它的初始状态(0)。在同步代码执行完毕后的某个时刻,state 才会增加到 1。而我们看到reduce方法中的减法却是同步打印出来了。

解读setState工作流

在上面的小🌰中,又引生出两个疑问:为什么前两个方法是异步的,而加了setTimeout又变成“同步”的了。在此先引用修言大佬的结论:这里我先给出一个结论:并不是setTimeout改变了setState,而是 setTimeout 帮助 setState “逃脱”了 React 对它的管控。只要是在 React 管控下的 setState,一定是异步的。

在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick 和浏览器里的 Event-Loop:每来一个 setState,就把它塞进一个队列里“攒起来”。等时机成熟,再把“攒起来”的 state 结果做合并,最后只针对最新的 state 值走一次更新流程。这个过程,叫作“批量更新”。

 this.setState({
     count: this.state.count + 1 ===> 入队,[count+1的任务]
     });
    this.setState({
    count: this.state.count + 1 ===> 入队, [count+1的任务,count+1的任务]
     });
                                      ↓
                                 合并 state,[count+1的任务]
                                      ↓
                                 执行 count+1的任务

这就不难理解为什么add和addDouble输出的值了,我们再回到为什么会有“同步”这个现象的产生。我们先来了解一下:setState底层执行过程:调用setState->计算优先级expirationTime->更新调度,调和fiber->合并state,执行render->commit更新真是DOM->执行回调函数。

而在官方文档的解释中:将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。在罕见的情况下,你需要强制 DOM 更新同步应用,你可以使用 flushSync 来包装它,但这可能会损害性能。React-dom提供了flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。

flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了。 关于setState工作流我们先引入这个流程图 image.png

batchingStrategy 正是 React 内部专门用于管控批量更新的对象