React中setState的异步与合并

时间:2024-04-16 11:44:50

场景一

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 (
            <div>
                <span>{this.state.val}</span>
            </div>
        )
    }
}

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

        答案是:控制台输出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 (
            <div>
                <span>{this.state.val}</span>
            </div>
        )
    }
}

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

        答案有两种可能

                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 <div>
        <span>{val}</span>
    </div>
}

场景四

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 <div>
        <span>{val}</span>
    </div>
}

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

        答案都是:控制台输出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 (
            <div>
                <span>{this.state.val}</span>
            </div>
        )
    }
}

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

相关文章