this.setState同步还是异步?

本文详细解析了React中setState方法的同步与异步特性,特别是在legacy和concurrent模式下的行为差异,以及如何通过回调函数、setTimeout和DOM原生事件实现同步更新。同时介绍了React的批处理更新机制和优先级控制方法。
摘要由CSDN通过智能技术生成

目录

拓展: React 是有多种模式

 1、this.setState的参数

1.1  假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?

2、是什么造成了this.setState()的不同步?

3、 那this.setState()什么时候同步,什么时候不同步?

3.1  异步的时候:

3.2  同步时候:比如用 promise 或者 setTimeout 时 批量更新规则被打破,就可以实现同步更新

3.2.1  使用setTimeout 使异步变成同步

3.2.2   那么,如何在如上异步环境下,继续开启批量更新模式呢?

3.2.3  那么如何提升更新优先级呢?

4、小示例:

4.1  onClick点击事件(),setState是异步的

4.2   DOM 原生事件 : setState是同步的



 this.setState是同步还是异步应该困扰好多人吧,然后百度一搜,众说纷纭,有说同步,有说异步...”到底是同步还是异步能不能直接了当说个结果呀!“,如果你也同样这样的想法,那今天说个明白:setState本身是异步的,但在特殊环境下(setTimeout、setInterval 等 DOM 原生事件)它是同步的。

拓展: React 是有多种模式

  • React 是有多种模式的,基本平时用的都是 legacy 模式下的 React,除了legacy 模式,还有 blocking 模式和 concurrent 模式, blocking 可以视为 concurrent 的优雅降级版本和过渡版本。React 最终目的,不久的未来将以 concurrent 模式作为默认版本,这个模式下会开启一些新功能。下面围绕 legacy 模式下的 state。

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

  • 通过 ReactDOM.render(<App />, rootNode) 方式创建应用,则为 legacy 模式,这也是 create-react-app 目前采用的默认模式;
  • 通过 ReactDOM.unstable_createRoot(rootNode).render(<App />) 方式创建的应用,则为 concurrent模式 。对于 concurrent 模式下,会采用不同 State 更新逻辑。前不久透露出未来的Reactv18 版本,concurrent 将作为一个稳定的功能出现。

 1、this.setState的参数

this.setState(param1, param2);
        param1: 对象或函数 --- 改变state的值
        param2: 回调函数 --- 等待state更新后,执行的函数

先看项目中遇到类似这样的问题:

【问题】

        发现 if 语句里拿不到type的值

【原因】

        this.setState()不保证是同步的,所以在if条件中调用setState修改后的值,是做不到更新

的。

【解决】

        利用this.setState()的第二个参数:回调函数,在等第一个参数内的state更新后再调用

 【问题】

        this.setState是异步更新,所以console.log(this.state.count)拿不到10

export default class A extends React.pureComponent {
    constructor(props){
        this.state={
            count: 0,
        }
    }    


    // 第一个参数:对象
add () {
    this.setState({
        count:10
    })
    
    console.log(this.state.count) // 这里打印this.state.count=0, 这里拿不到 this.state.count=10,  说明了this.setState更新是异步的
}


}

 【解决】

        假设你想同步获取值,setState还提供了回调函数,在回调函数里可以同步拿到console.log(this.state.count)等于10

// 第二个参数:回调函数,在等待第一个参数更新之后调用
// 假设你想同步获取值,setState还提提供一回调函数。
add () {
    this.setState({
        count:10
    },()=>{
    	
		console.log(this.state.count) // 这里是可以拿到的 this.state.count=10
	})
}
1.1  假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?
  • 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
  • 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
  • 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
  • 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。

                

请记住一个主要任务的先后顺序,这对于弄清渲染过程可能会有帮助:
render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback 。

2、是什么造成了this.setState()的不同步?

只是因为react的性能优化机制体现为异步。在react的生命周期函数或者作用域下为异步,但在特殊环境下(setTimeout、setInterval 等 DOM 原生事件)它是同步的。

setState默认是异步

        React18版本之后 setState默认是异步,假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnodediff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。

       

export default class index extends React.Component{
    state = { number:0 }
    handleClick= () => {
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback1', this.state.number)  })
          console.log(this.state.number)
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback2', this.state.number)  })
          console.log(this.state.number)
          this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback3', this.state.number)  })
          console.log(this.state.number)
    }
    render(){
        return <div>
            { this.state.number }
            <button onClick={ this.handleClick }  >number++</button>
        </div>
    }
} 

        点击打印:0, 0, 0,  callback1  1 ,callback2  1 ,callback3   1

        点击事件触发执行代码,setState是异步,碰到异步就跳过,先 执行console.log。console.log执行完,再从头开始执行异步代码

如上代码,在整个 React 上下文执行栈中会变成这样:

        

 react的性能优化机制:React 的批处理更新导致的(batch the updates)

React为了优化性能,setState()执行时会判断变量isBatchingUpdates的值是true or false, 然后决定是同步更新还是批量更新(结合上面handleClick点击方法的示例)

                

3、 那this.setState()什么时候同步,什么时候不同步?

3.1  异步的时候:

this.setState在React合成事件/钩子函数中,React会通过batchedUpdates()这个函数将isBatchingUpdates变成true,即批量更新的,是异步的。

在 React 事件执行之前通过 isBatchingUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingUpdates = false; 关闭开关
        

3.2  同步时候:比如用 promise 或者 setTimeout 时 批量更新规则被打破,就可以实现同步更新
3.2.1  使用setTimeout 使异步变成同步
setTimeout(()=>{
    this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback1', this.state.number)  })
    console.log(this.state.number)
    this.setState({ number:this.state.number + 1 },()=>{    console.log( 'callback2', this.state.number)  })
    console.log(this.state.number)
    this.setState({ number:this.state.number + 1 },()=>{   console.log( 'callback3', this.state.number)  })
    console.log(this.state.number)
})

打印 : callback1 1 , 1, callback2 2 , 2,callback3 3 , 3

                 

3.2.2   那么,如何在如上异步环境下,继续开启批量更新模式呢?

        React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新,可以将上述 setTimeout 里面的内容做如下修改:

import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM



setTimeout(()=>{
    unstable_batchedUpdates(()=>{
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1})
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number) 
    })
})

打印: 0 , 0 , 0 , callback1 1 , callback2 1 ,callback3 1

        在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变 state 会促使视图多次渲染。

3.2.3  那么如何提升更新优先级呢?

        React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。

handerClick=()=>{
    setTimeout(()=>{
        this.setState({ number: 1  })
    })
    this.setState({ number: 2  })
    ReactDOM.flushSync(()=>{
        this.setState({ number: 3  })
    })
    this.setState({ number: 4  })
}
render(){
   console.log(this.state.number)
   return ...
}

打印 3 4 1 

  • 首先 flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以 2 和 3 被批量更新到 3 ,所以 3 先被打印。
  • 更新为 4。
  • 最后更新 setTimeout 中的 number = 1。

flushSync补充说明

        flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。

综上所述, React 同一级别更新优先级关系是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

4、小示例:

        分别用两种方法绑定button的click事件,点击button的时候,改变state的值

4.1  onClick点击事件(),setState是异步的

       把这两次打印 console.log('prev state:', this.state.type)、console.log('current state:', this.state.type);  放到队列中,一起更新,所以第二次打印值与第一次打印值一样
  };

export default class A extends React.pureComponent {


constructor(props) {
    super(props);
    this.state = {
      type: 'origin', // 原始值为 origin
    };
  }
  

changeState = e => {
    console.log('prev state:', this.state.type);
    
    this.setState({
      type: 'changed', // 改变后为 changed
    });
    
    console.log('current state:', this.state.type);
  };



render() {
	console.log('render3');
    return (
      <Button onClick={this.changeState}>改变state</Button>
    );
  }

}

        打印结果:

        

       isBatchingUpdates是true,批量更新的,是不同步的,把要执行的内容放到队列中,一起更新,所以第二次打印值与第一次打印值一样

4.2   DOM 原生事件 : setState是同步的

         把这两次打印 console.log('prev state:', this.state.type)、console.log('current state:', this.state.type); 分别打印 ,所以第二次打印值与第一次打印值不一样
  };

export default class A extends React.pureComponent {

constructor(props) {
    super(props);
    this.state = {
      type: 'origin', // 原始值为 origin
    };
  }

componentDidMount = e => {
    const dom = document.getElementById('btn');
    dom.addEventListener('click', this.changeState);
}

 componentWillUnmount() {
        // 及时销毁自定义 DOM 事件
        document.body.removeEventListener('click', this.changeState)
    }
  

changeState = e => {
    console.log('prev state:', this.state.type);
    
    this.setState({
      type: 'changed', // 改变后为 changed
    });
    
    console.log('current state:', this.state.type);
  };


render() {
	console.log('render');
    return (
      <Button id="btn">改变state</Button>
    );
  }

}

        打印结果:

        

isBatchingUpdates是false,说明是同步更新的,一行执行完紧接着执行下一行。
(在输出prev state 这一行之后,遇到第二行的this.setState()就立即执行了,state更新之后就触发了render,然后才输出current state)

 写的太棒了


React 进阶实践指南 - 我不是外星人 - 掘金小册

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值