React18中setState更新机制

import React, { Component } from 'react'

 class App extends Component {
  state = {
    num:1
  }

  addNum4 = ()=>{
    this.setState({num:this.state.num + 1})
    console.log('setState1', this.state);//1
    this.setState({num:this.state.num + 1});
    console.log('setState2', this.state);//1

    setTimeout(()=>{
      this.setState({num:this.state.num + 1})
      console.log('setTime setState1',this.state);
      this.setState({num:this.state.num + 1});
      console.log('setTime setState2',this.state);
    })
  }

  render() {
    
    const {num} = this.state
    return (
      <div>
        <button onClick={this.addNum4}>测试四</button>
      </div>
    )
  }
}



export default App;

得到的结果为什么是这样
setState1 {num: 1}
setState2 {num: 1}
setTime setState1 {num: 2}
setTime setState2 {num: 2}

image.png

React18对自动批处理做出了一些更新,但是我对内部具体逻辑和实现还是不太明白
在React17中如果在setTimeout,setInterval等回调里面,更新就是同步的了,但是React18好像对这一点进行了更改???
https://github.com/reactwg/re...

阅读 4.3k
4 个回答

在React 18使用 createRoot就都是 batching了

浏览器事件循环

这种现象,建议去了解下浏览器事件循环。里面有讲到同步任务和异步任务。本人功力有限,且浏览器事件循环的知识点有点多就不再这里叙述了。先简单讲讲何为异步任务:

异步任务

异步任务分为宏任务和微任务,那么代码中的哪些操作是宏任务呢?哪些操作又是微任务呢?

能触发宏任务的代码

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering
  • ...

能触发微任务的代码

  • Promise.then ,这也是我们的常见操作
  • Object.observe
  • MutationObserver
  • ...

那么宏任务与微任务的区别是什么?请看下面的代码,它们的输出顺序是什么?

// console.log 是同步任务
console.log(1)
// setTimeout 是宏任务
setTimeout(() => {
    console.log(2)
}, 0)
// Promise.then 是微任务
Promise.resolve().then(() => {
    console.log(3)
})

输出的结果是:1、3、2。那么可以根据输出的结果得到一个结论:js的执行是优先同步任务 > 微任务 > 宏任务

回归正题,解答问题

根据题主的问题,再结合浏览器事件循环相关的知识点。就能很清楚的知道题主提供的代码为什么会是1、1、2、2

重要概念,请记住

  • 对于React中的setState更新操作来讲, setState执行的是一个微任务,会压到微任务队列中
  • setTimeout 是一个宏任务,添加一个宏任务队列。
  • console.log 是一个同步任务,会即时执行代码

根据浏览器事件循环机制,请看代码:

import React, { Component } from "react";

class App extends Component {
  state = {
    num: 1
  };

  addNum4 = () => {
    // 先压入微任务执行栈中,等待同步任务执行完成再执行该逻辑
    this.setState({ num: this.state.num + 1 });

    // 执行同步任务,这时候 num 还未更新,所以拿到的就是初始值1
    console.log("setState1", this.state); //1

    // 同理,等待同步任务执行完成再执行该逻辑
    this.setState({ num: this.state.num + 1 });

    // 执行同步任务,这时候 num 还未更新,所以拿到的就是初始值1
    console.log("setState2", this.state); //1
    
    // 先新增一个宏任务队列,等待同步任务和微任务执行完成
    setTimeout(() => {
      // 这里的代码如果开始执行了,那么说明上一次的同步任务和微任队列都执行完成了。
    
      // 这里又是微任务操作,所以先压入微任务队列,等待同步任务执行完成再执行该任务
      this.setState({ num: this.state.num + 1 });

      // 执行同步任务逻辑,由于微任务的执行优于宏任务,所以上一次的任务已经执行完成了
      // 所以这里拿到的是 2
      console.log("setTime setState1", this.state);

      // 同理,会先压入微任务队列,等待同步任务执行完成再执行该任务
      this.setState({ num: this.state.num + 1 });

      // 同理,这里拿到的是2
      console.log("setTime setState2", this.state);
    });
  };

  render() {
    const { num } = this.state;
    return (
      <div>
        <button onClick={this.addNum4}>测试四{num}</button>
      </div>
    );
  }
}

export default App;

最后

首先,打完收功。
其次,关于浏览器的事件循环机制有很多知识点,希望题主可以多查询下相关资料,这些知识点面试中也会问道。
还有,本文中对浏览器事件循环的回答并不全面,只能当做解答问题的引子。里面缺失了很多内容,不可在面试的过程中拿出来,怕坑到你。

新手上路,请多包涵

首先你要搞懂,setStatereact 里面的一个 钩子,setTimeout 是浏览器的方法。

react 渲染是 通过 state 改变时触发渲染,出于性能考虑也不是每次改变都触发一次渲染,而是每次渲染前,都执行一堆 setStatesetState 会经过 react 的钩子。

回到问题本身:
点击 addNum4,把事件添加到 执行栈 中,顺序执行代码。
首先执行了第一次 this.setState({num:this.state.num + 1}) ,这个时候的 this.state.num1 (初始值)。但是你函数没有走完,这时候你的第一次 setState 还没有生效,也只是添加到 react 的事务中。

这时候你执行的 console 结果当然还是 1

第二次执行 this.setState({num:this.state.num + 1}), 因为函数还没走完,所以你的 this.state.num 还是为 1

这时候你执行的 console 结果当然还是 1

现在遇到了 setTimeout 函数,添加到 定时触发器线程 内。

接着执行当前函数,遇到结束符号 },也没有其他内容,这时候开始执行渲染相关逻辑(就是 I/O 之类的),执行完后这个时候 任务队列内 没有内容(setTimeout还没到结束时间)

这个时候的 state.num 值是啥?
// 从上面分析可以看出,渲染前执行的事务队列中内容如下
[
this.setState({num:this.state.num + 1}),
this.setState({num:this.state.num + 1})
]
问题在于 当前的 state.num 值为什么,我们可以知道初始值是 1, 进来的时候非函数,而是一个对象,这里的实物队列其实是这样
[
this.setState({num:2}),
this.setState({num:2}) // 你是不是好奇这里为啥是 2?这个对象创建的时候,都是计算后的值,放入时的 num == 1, 所以这里是 2
]

执行完后,这里的 state.num 为 2

等待一些时间后,setTimeout 结束了,要执行的代码从 定时触发器线程 放到了 任务队列 中,浏览器接下来要执行 任务队列 中的内容,也就是 setTimeout 中的内容。

  • 知识点: 异步任务中的 state 变更都是同步的*
    执行 setTimeout 中第一个 setState ,输出为 3 (2+1)
    执行 setTimeout 中第二个 setState ,输出为 4 (3+1)

为啥你的输出是

setTime setState1 {num: 2}
setTime setState2 {num: 2}

为了确认,我在线执行了一下你的代码,结果如下
image.png

跟我结论一致。

我理解为,每一帧的num是state中num的快照。
第一次初始化

const num=0
let state={num}

第一次执行行为

state = {num:num+1}
console.log(state.num)// 1

无论你执行这句几次,state.num永远等于1,因为num是0

因为有state更新,所以触发第二次初始化

const num = state.num
state ={num}

这个时候再执行行为

state = {num:num+1}
console.log(state.num)// 2

无论你执行这句几次,state.num永远等于2,因为num是1
如果这个时候执行异步,就会在第下一次初始化后,执行行为之前运行异步

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题