场景一
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})}}
分析上述代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?
答案是:控制台输出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版本之前:控制台输出13610;页面上展示 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})}}
分析上述场景五代码,当页面加载完之后。控制台中会输出什么?页面上又会展示什么呢?