关于 setState

setState 的更新是同步还是异步,一直是人们津津乐道的话题。不过,实际上如果我们需要用到更新后的状态值,并不需要强依赖其同步/异步更新机制。在类组件中,我们可以通过this.setState的第二参数、componentDidMountcomponentDidUpdate等手段来取得更新后的值;而在函数式组件中,则可以通过useEffect来获取更新后的状态。所以这个问题,其实有点无聊。

不过,既然大家都这么乐于讨论,今天我们就系统地梳理一下这个问题,主要分为两方面来说:

  • 类组件(class-component)的更新机制
  • 函数式组件(function-component)的更新机制

类组件中的 this.setState

在类组件中,这个问题的答案是多样的,首先抛第一个结论:

  • legacy模式中,更新可能为同步,也可能为异步;
  • concurrent模式中,一定是异步。

问题一、legacy 模式和 concurrent 模式是什么鬼?

  • 通过ReactDOM.render(<App />, rootNode)方式创建应用,则为 legacy 模式,这也是create-react-app目前采用的默认模式;

  • 通过ReactDOM.unstable_createRoot(rootNode).render(<App />)方式创建的应用,则为concurrent模式,这个模式目前只是一个实验阶段的产物,还不成熟。

legacy 模式下可能同步,也可能异步?

是的,这不是玄学,我们来先抛出结论,再来逐步解释它。

  1. 当直接调用时this.setState时,为异步更新;
  2. 当在异步函数的回调中调用this.setState,则为同步更新;
  3. 当放在自定义 DOM 事件的处理函数中时,也是同步更新。

实验代码如下:

class StateDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            count: 0
        }
    }
    render() {
        return <div>
            <p>{this.state.count}</p>
            <button onClick={this.increase}>累加</button>
        </div>
    }
    increase = () => {
        this.setState({
            count: this.state.count + 1
        })
        // 异步的,拿不到最新值
        console.log('count', this.state.count)

        // setTimeout 中 setState 是同步的
        setTimeout(() => {
            this.setState({
                count: this.state.count + 1
            })
            // 同步的,可以拿到
            console.log('count in setTimeout', this.state.count)
        }, 0)
    }

    bodyClickHandler = () => {
        this.setState({
            count: this.state.count + 1
        })
        // 可以取到最新值
        console.log('count in body event', this.state.count)
    }

    componentDidMount() {
        // 自己定义的 DOM 事件,setState 是同步的
        document.body.addEventListener('click', this.bodyClickHandler)
    }
    componentWillUnmount() {
        // 及时销毁自定义 DOM 事件
        document.body.removeEventListener('click', this.bodyClickHandler)
    }
}

要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。

首先我们来看看 setState 的主流程:

  1. 调用this.setState(newState)
  2. newState会存入 pending 队列; 3,判断是不是batchUpdate; 4,如果是batchUpdate,则将组件先保存在所谓的脏组件dirtyComponents中;如果不是batchUpdate,那么就遍历所有的脏组件,并更新它们。

由此我们可以判定:所谓的异步更新,都命中了batchUpdate,先保存在脏组件中就完事;而同步更新,总是会去更新所有的脏组件。

非常有意思,看来是否命中batchUpdate是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?

这就涉及到一个很有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些我们看不到的东西,现在我们通过魔法让其显现出来:

increase = () => {
        // 开始:默认处于bashUpdate
        // isBatchingUpdates = true
        this.setState({
            count: this.state.count + 1
        })
        console.log('count', this.state.count)
        // 结束
        // isBatchingUpdates = false

    }
    increase = () => {
        // 开始:默认处于bashUpdate
        // isBatchingUpdates = true
        setTimeout(() => {
            // 此时isBatchingUpdates已经设置为了false
            this.setState({
                count: this.state.count + 1
            })
            console.log('count in setTimeout', this.state.count)
        }, 0)
        // 结束
        // isBatchingUpdates = false
    }

当 react 执行我们所书写的函数时,会默认在首位设置isBatchingUpdates变量。看到其中的差异了吗?当 setTimeout 执行其回调时,isBatchingUpdates早已经在同步代码的末尾被置为false了,所以没命中batchUpdate

那自定义 DOM 事件又是怎么回事?代码依然如下:

  componentDidMount() {
    // 开始:默认处于bashUpdate
    // isBatchingUpdates = true
    document.body.addEventListener("click", () => {
      // 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了
      this.setState({
        count: this.state.count + 1,
      });
      console.log("count in body event", this.state.count); // 可以取到最新值。
    });
    // 结束
    // isBatchingUpdates = false
  }

我们可以看到,当componentDidMount跑完时,isBatchingUpdates已经设置为false了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates当然也是false,不会命中batchUpdate机制。

总结:

  • this.setState是同步还是异步,关键就是看能否命中batchUpdate机制
  • 能不能命中,就是看isBatchingUpdatestrue还是false
  • 能命中batchUpdate的场景包括:生命周期和其调用函数、React中注册的事件和其调用函数。总之,是React可以“管理”的入口,关键是“入口”。

这里要注意一点:React去加isBatchingUpdate的行为不是针对“函数”,而是针对“入口”。比如setTimeout、setInterval、自定义DOM事件的回调等,这些都是React“管不到”的入口,所以不会去其首尾设置isBatchingUpdates变量。

concurrent 模式一定是异步更新

因为这个东西只在实验阶段,所以要开启 concurrent 模式,同样需要将 react 升级为实验版本,安装如下依赖:

npm install react@experimental react-dom@experimental

其他代码不用变,只更改 index 文件如下:

- ReactDOM.render(<App />, document.getElementById('root'));

+ ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);

则可以发现:其更新都是同步的,在任何情况下都是如此。

关于函数式组件中 useState 的 setter

在函数式组件中,我们会这样定义状态:

const [count, setCount] = useState(0)

这时候,我们发现当我们无论在同步函数还是在异步回调中调用 setCount 时,打印出来的 count 都是旧值,这时候我们会说:setCount 是异步的。

  const [count, setCount] = useState(0);

  // 直接调用
  const handleStrightUpdate = () => {
    setCount(1);
    console.log(count); // 0
  };

  // 放在setTimeout回调中
  const handleSetTimeoutUpdate = () => {
    setTimeout(() => {
      setCount(1);
      console.log(count); // 0
    });
  };

setCount 是异步的,这确实没错,但是产生上述现象的原因不只是异步更新这么简单。原因主要有以下两点:

1,调用 setCount 时,会做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders),从这个角度上来说,setCount确实是一个异步操作;

2,函数式的capture-value特性决定了console.log(count)语句打印的始终是一个只存在于当前帧的常量,所以就算无论 setCount 是不是同步的,这里都会打印出旧值。

深入剖析setState同步异步机制的更多相关文章

  1. setState同步异步场景

    setState同步异步场景 React通过this.state来访问state,通过this.setState()方法来更新state,当this.setState()方法被调用的时候,React会 ...

  2. Python并发编程系列之常用概念剖析:并行 串行 并发 同步 异步 阻塞 非阻塞 进程 线程 协程

    1 引言 并发.并行.串行.同步.异步.阻塞.非阻塞.进程.线程.协程是并发编程中的常见概念,相似却也有却不尽相同,令人头痛,这一篇博文中我们来区分一下这些概念. 2 并发与并行 在解释并发与并行之前 ...

  3. js的线程和同步异步以及console.log机制

    项目上线了,闲下来就写写东西吧.积累了好多东西都没有做笔记~挑几个印象深刻的记录一下吧. js的同步异步以及单线程问题: 都知道单线程是js的一大特性.但是通常io(ajax获取服务器数据).用户/浏 ...

  4. 同步异步,阻塞非阻塞 和nginx的IO模型

    同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication).所谓同步,就是在发出一个*调用*时,在没有得 ...

  5. 进程&线程 同步异步&阻塞非阻塞

    2015-08-19 15:23:38 周三 线程 线程安全 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码 线程安全问题都是由全局变量及静态变量引起的 若每个线程中对 ...

  6. I/O阻塞非阻塞,同步异步

    http://www.cnblogs.com/luotianshuai/p/5098408.html "阻塞"与"非阻塞"与"同步"与&qu ...

  7. 【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

    开始准备看Java NIO的,这篇文章:http://xly1981.iteye.com/blog/1735862 里面提到了这篇文章 http://xmuzyq.iteye.com/blog/783 ...

  8. JavaScript的异步机制

    我们经常说JS是单线程的,比如node.js研讨会上大家都说JS的特色之一是单线程的,这样使JS更简单明了,可是大家真的理解所谓JS的单线程机制吗?单线程时,基于事件的异步机制又该当如何 1 先看下两 ...

  9. JavaScript单线程和异步机制

    随着对JavaScript学习的深入和实践经验的积累,一些原理和底层的东西也开始逐渐了解.早先也看过一些关于js单线程和事件循环的文章,不过当时看的似懂非懂,只留了一个大概的印象:浏览器中的js程序时 ...

随机推荐

  1. go并发之goroutine和channel,并发控制入门篇

    并发的概念及其重要性 这段是简单科普,大佬可以跳过 并发:并发程序指同时进行多个任务的程序.在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行 ...

  2. IOS实现自动定位和手动选择城市功能

    IOS自动定位使用的是高德地图SDK 在高德开放平台http://lbs.amap.com/api/ios-sdk/down/ 下载2D地图SDK和搜索SDK 将SDK导入工程内 按照高德的配置说明进 ...

  3. 判断一个对象是否为空?怎么得到一个对象的第几个键名(key)?

    var obj = {"微信":[],"qq":[]} console.log( Object.keys(obj) ) // ["微信",& ...

  4. js下 Day09、事件(二)

    一.事件流 事件流描述的是从页面中接收事件的顺序,目前主要有三个模型: #1. 事件冒泡: 事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的元素

  5. .Net 常用ORM框架对比:EF Core、FreeSql、SqlSuger

    前言: 最近由于工作需要,需要选用一种ORM框架,也因此对EF Core.FreeSql.SqlSuger作简单对比.个人认为各有有优势,存在即合理,不然早就被淘汰了是吧,所以如何选择因人而议.因项目 ...

  6. 微服务痛点-基于Dubbo + Seata的分布式事务(AT)模式

    前言 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务.Seata 将为用户提供了 AT.TCC.SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案. ...

  7. java基础:switch语句应用,循环的详细介绍以及使用,附练习案列

    1. switch语句 1.1 分支语句switch语句 格式 switch (表达式) { case 1: 语句体1; break; case 2: 语句体2; break; ... default ...

  8. SpringBoot整合任务调度框架Quartz及持久化配置

    目录 本篇要点 SpringBoot与Quartz单机版快速整合 引入依赖 创建Job 调度器Scheduler绑定 自动配置,这里演示SimpleScheduleBuilder 手动配置,这里演示C ...

  9. tomcat能正常启动,但是http://localhost:8080/网页就是打不开,报404

    问题描述: 在IDE中创建了一个新的Servers,并且加入一个Tomcat.然后启动服务,进入浏览器,输入localhost:8080进入,显示错误.服务是可以正常启动的,而且没有任何异常. 问题描 ...

  10. 扫盲:Kotlin 的泛型

    引子 相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼. Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin ...