• React中setState的异步与合并


    场景一

    1. import React from 'react';
    2. class MyApp extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.state = {
    6. val: 0
    7. }
    8. }
    9. componentDidMount() {
    10. this.setState({ val: this.state.val + 1 })
    11. console.log(this.state.val)
    12. this.setState({ val: this.state.val + 2 })
    13. console.log(this.state.val)
    14. this.setState((prevState, props) => {
    15. return {
    16. val: prevState.val + 3
    17. }
    18. })
    19. console.log(this.state.val)
    20. this.setState((prevState, props) => {
    21. return {
    22. val: prevState.val + 4
    23. }
    24. })
    25. console.log(this.state.val)
    26. }
    27. render() {
    28. return (
    29. <div>
    30. <span>{this.state.val}span>
    31. div>
    32. )
    33. }
    34. }

    分析上述代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?

            答案是:控制台输出0 0 0 0;页面上展示 9

            简单解释:setState在上述代码中是异步的。如果传入的是对象,则会合并之前的;如果传入的是函数,则不合并。

    详细分析:

            react中setState有两种传参方式来更新状态(也就是来修改state的值):

                    第一种是传入新的state对象。例如上面代码12和14行

                    第二种是传入一个函数,并且在回调函数里返回新的state对象。例如上面代码16和22行

            当我们在更新state时,如果一个函数中,多次调用setState方法

                    如果当前传入的是一个state对象,则React会将当前对象与之前的传入的对象进行合并处理,如果之前存在对同一个状态的更新,则会覆盖。

                    如果当前传入的是一个函数,则React会按照各个setState的调用顺序,将它们依次存入一个队列,然后在进行状态更新的时候,按照队列顺序依次调用,并将上一个调用结束时产生最新的state传入下一个调用函数中。(我原本以为是因为函数的内存地址不一致导致的,经实验发现即使传入相同的函数,也不会覆盖上一个setState)

                    既然要合并并且要依次添加到队列中,那么肯定不能立即处理每一次的更新。只能等当前函数结束之后,再统一处理。这么做也是为了允许React批量处理多个状态更新,以提高性能。因此在这种情况,setState可以理解为是异步更新的。这也能够解释为什么不建议我们使用当前值去计算下一个state的值。

    场景二

    1. import React from 'react';
    2. class MyApp extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.state = {
    6. val: 0
    7. }
    8. }
    9. componentDidMount() {
    10. setTimeout(() => {
    11. this.setState({ val: this.state.val + 1 })
    12. console.log(this.state.val)
    13. this.setState({ val: this.state.val + 2 })
    14. console.log(this.state.val)
    15. this.setState((prevState, props) => {
    16. return {
    17. val: prevState.val + 3
    18. }
    19. })
    20. console.log(this.state.val)
    21. this.setState((prevState, props) => {
    22. return {
    23. val: prevState.val + 4
    24. }
    25. })
    26. console.log(this.state.val)
    27. });
    28. }
    29. render() {
    30. return (
    31. <div>
    32. <span>{this.state.val}span>
    33. div>
    34. )
    35. }
    36. }

    分析上述代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?

            答案有两种可能

                    React18版本之前:控制台输出1 3 6 10;页面上展示 10

                    React18版本后:控制台输出0 0 0 0;页面上展示 9

            简单解释:

                    在React18之前,如果在setTimeout中调用useState,setState是同步的,并且不管是传入对象还是函数,都不合并;

                    在React18之后。则跟场景一保持一致了,setState是异步,并且合并setState传入对象的情况,函数依旧不合并。

    详细分析:
            在React18之前:当你在setTimeout、setInterval、或其他原生DOM事件监听器的回调中调用useState时,它会是同步的。在这些情况下,React不会进行批量更新,而是立即应用状态更新。

            而在React18之后。React引入了新的并发模式(Concurrent Mode),在这种模式下,所有的状态更新默认都是异步的,无论你在哪里调用它们。这是为了支持更复杂的应用程序,在这些应用程序中,React需要在不阻塞用户界面的情况下,管理多个长时间运行的任务。

    场景三

    1. function MyApp() {
    2. const [val, setVal] = React.useState(0);
    3. React.useEffect(() => {
    4. setVal(val + 1);
    5. console.log(val);
    6. setVal(val + 2);
    7. console.log(val);
    8. setVal(val => val + 3);
    9. console.log(val);
    10. setVal(val => val + 4);
    11. console.log(val);
    12. }, []);
    13. return <div>
    14. <span>{val}span>
    15. div>
    16. }

    场景四

    1. function MyApp() {
    2. const [val, setVal] = React.useState(0);
    3. React.useEffect(() => {
    4. setTimeout(() => {
    5. setVal(val + 1);
    6. console.log(val);
    7. setVal(val + 2);
    8. console.log(val);
    9. setVal(val => val + 3);
    10. console.log(val);
    11. setVal(val => val + 4);
    12. console.log(val);
    13. })
    14. }, []);
    15. return <div>
    16. <span>{val}span>
    17. div>
    18. }

    分析上述场景三场景四代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?

            答案都是:控制台输出0 0 0 0;页面上展示 9

            简单解释:从React16.8诞生hook以来,使用useState来改变状态,不管是在setTimeout之内,还是普通函数中,setState都是异步的。并且对象会合并,函数不合并。

    详细分析:

            你可能会好奇,为什么在React16.8中的setTimeout中调用useState,setState竟然会合并,并且是异步的。关于这一点,笔者研究了好久,才勉强搞懂。可能笔者水平有限,如果解释不对的话,欢迎大佬指正。

            我们知道,函数组件是纯函数,执行完即销毁。因此无论组件初始化(render)还是组件更新(re-render)都会重新执行一次这个函数,获取最新的组件。这一点跟class组件不同,class组件是有实例的,因此执行完也还会存在,每次更新也都是同一个实例。

    详细步骤:

            1、在场景4的代码中,执行到第6行,确实同步执行了,但是是重新打开了一个函数,在新函数中,val变为了1。

            2、这时候我们回到旧函数,这里的val还是0,因此第7行输出0。

            3、执行第8行时,又重新打开了一个新函数,在新函数中,val变为了2。

            4、重新回到旧函数,这里的val还是0,因此第9行输出0。

            5、执行第10行时,我们之前讲过,如果是函数的话,会拿到最新的状态,并更新,因此在新函数中,val变为了5。

            6、重新回到旧函数,执行第11行的时候,跟之前一样,输出0。

            7、执行12行,跟第5步同理,val变为了9。

            8、执行13行,输出0。

            9、执行完毕,因此控制台打印0 0 0 0 ,页面输出9

    补充知识点:

            既然函数组件每次都销毁,那么我们怎么能保证数据不会丢失呢,这时候就需要一个很神奇的东西了——hook。hook会对数据进行一个保存,当函数第一次执行时,hoock会存储下状态的初始值。每次数据更新,重新加载函数时,会按照hook顺序依次将最新的数据传入新的函数hook中。

            这也是为什么hook严重依赖执行顺序,一定要放在函数第一层,不能放在if、for中,如果放在判断语句中。如果if这次是true,下次函数执行变成false了,那么顺序就会改变,数据则混乱。

    总结

            只有在React18之前版本的class组件中的setTimeout中调用useState,setState是同步的,状态都不合并

            其他所有情况的setState都是异步,传入对象合并,传入函数不合并

    场景五(彩蛋)

            留个作业,嘿嘿嘿。

            将场景一中的两个函数更新state移动到了对象更新state上面

    1. import React from 'react';
    2. class MyApp extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.state = {
    6. val: 0
    7. }
    8. }
    9. componentDidMount() {
    10. this.setState((prevState, props) => {
    11. return {
    12. val: prevState.val + 1
    13. }
    14. })
    15. console.log(this.state.val)
    16. this.setState((prevState, props) => {
    17. return {
    18. val: prevState.val + 2
    19. }
    20. })
    21. console.log(this.state.val)
    22. this.setState({ val: this.state.val + 3 })
    23. console.log(this.state.val)
    24. }
    25. render() {
    26. return (
    27. <div>
    28. <span>{this.state.val}span>
    29. div>
    30. )
    31. }
    32. }

    分析上述场景五代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?

  • 相关阅读:
    嘉立创使用技巧
    软件测试面试会问哪些问题?
    C语言sizeof()计算空间大小为8的问题
    Spring AOP快速入门----XML方式和注解方式
    TensorFlow在推荐系统中的分布式训练优化实践
    P3387 【模板】缩点 Tarjan强连通分量/树上dp
    9.5QTday6作业
    扬帆牧哲:shopee台湾站点卖什么?
    C语言文本为什么不包括库函数和预处理命令
    R语言:卡方检验
  • 原文地址:https://blog.csdn.net/qq_36647492/article/details/136395865