React中setState的异步与合并

2024-03-01 1093阅读

温馨提示:这篇文章已超过401天没有更新,请注意相关的内容是否还可用!

场景一

import React from 'react';
class MyApp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            val: 0
        }
    }
    componentDidMount() {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
        this.setState({ val: this.state.val + 2 })
        console.log(this.state.val)
        this.setState((prevState, props) => {
            return {
                val: prevState.val + 3
            }
        })
        console.log(this.state.val)
        this.setState((prevState, props) => {
            return {
                val: prevState.val + 4
            }
        })
        console.log(this.state.val)
    }
    render() {
        return (
            
                {this.state.val}
            
        )
    }
}

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

React中setState的异步与合并
(图片来源网络,侵删)

        答案是:控制台输出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的值。

场景二

import React from 'react';
class MyApp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            val: 0
        }
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ val: this.state.val + 1 })
            console.log(this.state.val)
            this.setState({ val: this.state.val + 2 })
            console.log(this.state.val)
            this.setState((prevState, props) => {
                return {
                    val: prevState.val + 3
                }
            })
            console.log(this.state.val)
            this.setState((prevState, props) => {
                return {
                    val: prevState.val + 4
                }
            })
            console.log(this.state.val)
        });
    }
    render() {
        return (
            
                {this.state.val}
            
        )
    }
}

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

        答案有两种可能

                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需要在不阻塞用户界面的情况下,管理多个长时间运行的任务。

场景三

function MyApp() {
    const [val, setVal] = React.useState(0);
    React.useEffect(() => {
        setVal(val + 1);
        console.log(val);
        setVal(val + 2);
        console.log(val);
        setVal(val => val + 3);
        console.log(val);
        setVal(val => val + 4);
        console.log(val);
    }, []);
    return 
        {val}
    
}

场景四

function MyApp() {
    const [val, setVal] = React.useState(0);
    React.useEffect(() => {
        setTimeout(() => {
            setVal(val + 1);
            console.log(val);
            setVal(val + 2);
            console.log(val);
            setVal(val => val + 3);
            console.log(val);
            setVal(val => val + 4);
            console.log(val);
        })
    }, []);
    return 
        {val}
    
}

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

        答案都是:控制台输出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上面

import React from 'react';
class MyApp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            val: 0
        }
    }
    componentDidMount() {
        this.setState((prevState, props) => {
            return {
                val: prevState.val + 1
            }
        })
        console.log(this.state.val)
        this.setState((prevState, props) => {
            return {
                val: prevState.val + 2
            }
        })
        console.log(this.state.val)
        this.setState({ val: this.state.val + 3 })
        console.log(this.state.val)
    }
    render() {
        return (
            
                {this.state.val}
            
        )
    }
}

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

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]