阅读 923

setState和useState的set函数(usestate和setstate的区别)

学习过React的朋友们应该都知道setState和useState,两者在React的开发中具有了极其重要的作用,它们一个是类组件中改变状态的方法,另一个是大名鼎鼎的hooks中的一员,在函数式组件中极为常见,但是很多初学者在学习这两个函数时都是半知半解的状态,搞不懂它们到底是同步的?还是异步的?两者有什么区别没有?希望看完这篇文章之后大家都够有所收获!

setState同步异步问题

那我们就先从类组件中的setState来入手吧!在探究之前,我们需要先知道类组件的一些性质以及使用setState的注意事项:

  1. 首先我们要知道React中constructor是唯一可以初始化state的地方,也可以把它理解成一个钩子函数,该函数只会在组件第一次挂载到页面时才会执行一次,后续组件由于setState状态更新而重新渲染时都不会重新执行constructor函数了,并且constructor函数是在render函数和componentDidMount前执行的,这一点跟hooks中的useEffect有点不一样,后者第一次执行是在组件挂载到页面上后,类似于componentDidMount这个周期函数的执行

  1. 更新状态不要直接修改this.state,这也是纯函数的思想之一。虽然状态是可以改变的,但源码中是会进行新旧state比较的,由于这样操作会导致前后state的引用也就是地址相同,在这种情况下是不会触发组件更新的

  1. 再说说setState方法的使用吧!其主要作用相信大家都不陌生,就是去改变当前组件保存的状态,该方法接受两个参数,即setState(stateChange[, callback])

  • 第一个参数可以直接是新状态对应的对象,也可以是一个函数,react会自动判别传进去的是一个对象还是函数;如果传递进去的是一个函数,那么react会自动帮我们调用它,并在调用时会传递过来两个参数,第一个是前一次的状态,第二个是从父组件接受到的props值,我们可以利用这两个参数确定返回值,这个返回值就是我们想要更新的新状态所对应的对象

// 第一个参数直接传递一个对象 clickFn () {     this.setState({         count: 1     }) } // 第一个参数是一个函数,该函数的返回值会被当做要更新的对象 clickFn () {     this.setState((state, props) => {      return state.count + 1     }) } 复制代码

  • 第二个参数是一个回调函数(非必传),该回调函数会在状态改变成功之后自动执行,在该函数中我们就可以通过this.state获取最新的值去执行相关操作了

clickFn () {     this.setState({         count: 2     }, () => {         console.log(this.state.count)  // 2     }) } clickFn () { this.setState((preState, props) => {        return {...preState, count: preState.count + 1}     }, () => {        console.log(this.state.count); // 2     }) } 复制代码

清楚了上面几个知识点之后,我们就可以正式开始研究setState是同步还是异步的问题了,大多数人理解的setState可能是异步的,因为react官方中给出了这样一段话,导致很多人误解无论setState在哪里执行,其都是异步的

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.

// setState函数执行了之后不总是会立即更新组件,他可能会批量处理更新或将更新推迟到以后。这可能会导致执行了setState函数后立即读取this.state会陷入潜在的陷阱

我们可以先了解执行了setState之后会发生什么?

  • setState内部执行的过程是很复杂的,大致过程包括了更新state、创建新的节点,再经过diff算法比对差异,决定需要重新渲染哪一部分的内容,最终形成新的页面

从这些点我们可以大致地知道setState都做了哪些操作,其实还是蛮多的。如果每执行一次setState函数,这个过程都要完整的走一次,diff算法的比较以及Dom的更新都会在一定程度上消耗性能,这样雪球越滚越大之后最终就可能会出现性能上的问题

React肯定是考虑到了这些的,所以在源码中对setState函数做了一些处理,多个连续的setState函数所对应的新状态可能会在执行过程中被合并,最终再一次性更新完毕,这也可能是大部分人所认为setState函数异步更新,下面我们来看几个Demo:

export default class test extends PureComponent {     constructor() {         super()         this.state = {             count: 1         }     }     increase() {         this.setState({             count: this.state.count + 1         })         console.log(this.state.count); // setState函数执行了一次时,这里打印出来的count值还是1,说明状态并没有被同步更改,在这里setState是异步的     }     render() {         return (             <button onClick={e => this.increase()}>                 {this.state.count}             </button>         )     } } 复制代码

componentDidMount() {   document.querySelector('button').addEventListener('click', e => {     this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // setState函数执行了一次时,这里打印出来的count值为2,说明状态并已经被同步更改,在这里setState是同步的   }) } 复制代码

通过上面一个实例,可能打破了那些一贯认为setState是异步的那些人的认知,通过js的事件addEventListener绑定事件会发现,在setState执行完之后居然能够打印出更新过后的状态值,简直是匪夷所思。其实,我们并不能绝对地说setState到底是同步的还是异步的,这个是需要视情况而定的。如果我们想要真正的了解到为什么setState有时同步,有时异步,那我们就必须要知道React对setState到底做了什么?

React实际上为useState的set函数/setState 前后各加了段逻辑给包了起来。在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdatesisBatchingUpdates修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state,而是将新状态放到队列中延时更新。一般来说,只要是在react控制范围内的事件内使用setState,setState就是异步更新的,否则,setState就是同步更新

那么到底在哪些具体情况下是同步的,哪些是异步的呢?举个典型的例子:我们通常会给某个标签绑定onClick点击事件,由于 react 的事件委托机制,调用 onClick 执行的事件,像这种正常的react事件流是处于 react 的控制范围内的,所以其对应函数中的setState是异步更新的;比较典型的还有setTimeout,我们通常利用它做一些定时操作,但是如果在其内部使用setState,我们会发现它其实是同步更新的,这是因为 setTimeout 已经超出了 react 的控制范围,react 无法对 setTimeout 的代码前后加上事务逻辑(除非 react 重写 setTimeout

所以当遇到 setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调时,react 都是无法控制的,则如果在他们所对应的作用域下使用setState,那么此时setState毫无疑问就是同步的;为什么要搞清楚这些问题呢?因为在开发中的很多场景你可能需要连续多次调用setState函数,此时遇到问题你就可以快速地分析出解决方法

下面是我就从实际开发的角度展示的demo:

increase() {   Promise.resolve().then(res => { // then回调是放在异步队列中执行的,当点击了一次按钮之后,该函数才会被执行,此处使用promise模拟点击按钮之后发送网络请求,then回调模拟将接收到响应数据更新到状态中     this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // 2     this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // 3     this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // 4   }) } 复制代码

可以看到每次setState之后,我们打印出来的都是最新的状态,从而才能做到数字的叠加;不过这里有个注意的点就是:每次setState执行时,组件都会被重新渲染一次,render函数也会执行,所以如果以后真的遇到了这种情况,想在异步任务中使用多个setState又不想组件多次渲染,可以考虑把用到的状态都放到一个对象中集中管理,这样就只需要使用一次setState更新这个对象就行,render函数也只会重新渲染一次

但并不是说setState是异步执行的就不会遇到问题了,比如下面的demo:

// 用户点击了按钮之后执行increase函数,该函数是通过react内部的onClick来绑定的 increase() {   this.setState({     count: this.state.count + 1   })   console.log(this.state.count); // 1   this.setState({     count: this.state.count + 1   })   console.log(this.state.count); // 1   this.setState({     count: this.state.count + 1   })   console.log(this.state.count); // 1 } 复制代码

我们会发现,多次执行了setState函数来更新状态,但是state并没有立即更新,说明setState在这种情况下是异步执行的,render函数只会渲染一次,但是问题就来了,我在这里执行了3次setState,理应上最新状态应该为4才对,但是值却为2,这个正是react官方所说的“陷阱”。

由于每次执行setState都是通过this.state来获取最新状态,但是setState如果是异步更新的话不会立马就将状态改变,自然获取到的都是旧的状态,相当于执行了3次setState({count: 1 + 1})操作,最终react会通过Array.assign函数合并存储在队列中的新旧状态,导致最终更新的状态是{count: 2},所以页面中显示的数字就是2,而不是4

如果想要解决上述问题应该怎么办呢?下面我个人提供几种方法:

  1. 我们恰好可以利用setState第一个参数可以是函数的性质,react调用该函数时会把要更新的最新状态以及props传递进来,我们就可以通过preStatae来获取到最新一次的状态(这个新状态可能还没有被更新,但是我们可以获取到)

increase() {   this.setState(preState => ({ ...preState, count: preState.count + 1 }))   console.log(this.state.count);   this.setState(preState => ({ ...preState, count: preState.count + 1 }))   console.log(this.state.count);   this.setState(preState => ({ ...preState, count: preState.count + 1 }))   console.log(this.state.count); } 复制代码

改成这样操作后,在每个setState函数里面都可以获取到最新的state,比如第一个setState函数的参数preState.count值为1,第二个为2,第三个为3,实质上执行的操作就是:

increase() {   this.setState({ count: 2 })   this.setState({ count: 3 })   this.setState({ count: 4 }) } 复制代码

react在队列中合成这几个状态时,通过调用Array.assignapi,新状态会依次覆盖掉旧状态,所以最终react实际上要更新的状态就是{count:4},rende函数也只重新执行了一次,所以最终我们在页面中看到的数值就是4

  1. 利用asyncawait将异步更新转化为同步更新

increase() {   const handleFn = async () => {     await this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // 2     await this.setState({       count: this.state.count + 1     })          console.log(this.state.count); // 3     await this.setState({       count: this.state.count + 1     })     console.log(this.state.count); // 4   }   handleFn() } 复制代码

  1. 利用setTimeout将setState转为同步

increase() {   setTimeout(() => {     this.setState({count: this.state.count + 1})     console.log(this.state.count); // 2     this.setState({count: this.state.count + 1})     console.log(this.state.count); // 3     this.setState({count: this.state.count + 1})     console.log(this.state.count); // 4   }, 0) } 复制代码

个人推荐第一种方法,第二种和第三种都是将setState暴力的转为同步更新,这样子没执行一次setState,组件和render函数就重新执行一次,有点消耗性能

类组件的setState就介绍到这里了,如果有人想要深入了解的,那么我认为可以去看看react的源码,不过建议等react运用熟练了之后再进行源码的阅读,要不然学习起来会有点吃力;

useState的set函数同步异步问题

话不多说,接下来我们就进入hooks中的useState所返回的set函数解读,但其实useState的set函数和类组件的setState函数有跟大的相似之处,我们直接看几个Demo吧!

import React, { memo, useEffect, useRef, useState } from 'react' export default memo(function Test() {     const [count, setCount] = useState(1)     const clickFn = () => {         setCount(count + 1)         setCount(count + 1)         setCount(count + 1)         console.log(count); // 1     }     return (         <div onClick={clickFn}>             {count}         </div>     ) }) 复制代码

连续调用了三次set函数之后,打印出的count值还是为1,说明setCount在react事件流里面也是异步更新的,组件和render函数只会被重新渲染一次,但是屏幕中显示的数字并不是4,而是2。

  1. set函数内部执行的操作和useState有些许不同,useState异步执行时是会将要更新的状态放到一个队列中去的,而set函数是每执行一次都会将新状态替换旧状态的,但并不一定会立即渲染组件。

  2. 如果set函数确定是同步执行的了,那么每执行set函数一次,组件都会被重新渲染;如果set函数确定是异步执行的了,其内部其实是有一个防抖优化的,连续的更新同一个状态会使得内部状态不断地被替换,最终留下的是最新的那个。在此例中,相当于执行了3次setCount(2),最终替换为新状态的当然是数值2了

如果想要执行set函数之后count变为4,解决方法还是类似的

const clickFn = () => {   setCount(state => state + 1) // state的值为1   setCount(state => state + 1) // state的值为2   setCount(state => state + 1) // state的值为3   console.log(count); // 1 } 复制代码

状态仍然是异步更新,组件也只重新渲染了一次。所以打印出的值是旧状态,但是渲染到页面上的值是4

此时重点来了,那我们是不是也能用跟setState解决类似问题中所用到的setTimeout方法和async结合await的方法解决类似问题呢?我们来测试一下:

export default memo(function Test() {     const [count, setCount] = useState(1)     console.log('组件渲染了,count的值为' + count)     const clickFn = () => {         setTimeout(() => {             setCount(count + 1)             console.log(count); // 1             setCount(count + 1)             console.log(count); // 1             setCount(count + 1)             console.log(count); // 1         }, 0)     }     return (         <div onClick={clickFn}>             {count}         </div>     ) }) 复制代码

当点击了一次按钮后,控制台打印的数据为:

伪原创工具 SEO网站优化  https://www.237it.com/ 

可以看到在set函数下方再打印count的值,仍然是旧的状态,跟类组件中的setState有所不同,如果是后者,那么打印出来的应该是最新的状态才对,难道set函数在setTimeout里面变为了异步执行的吗?

并不是,可以再控制台看到,当第一次执行了setCount函数的时候,组件就会重新渲染,第二次执行setCount函数时,组件也会重新渲染。这就意味这在setTimeout中的set函数仍然还是同步执行的,但是在执行完set函数之后,当前作用域中的count并没有发生改变,所以在当前作用域下获取count的值仍然为旧的状态,但是在新渲染的组件作用域中就可以访问到最新的状态,count打印出来是2;但是这样问题就来了,由于当前作用域下状态并没有改变,setCount的参数永远都是2,所以并不能达到我们想要的目的;使用async结合await的方法也还是会遇到这个问题,所以我们在处理这种类型的问题时,还是使用给set函数传一个函数的方法比较稳妥一点

不过看到上面的打印结果,有些人可能会疑惑为什么第三次执行set函数组件没有重新渲染,这就涉及到另外一个知识点了:react会对新旧状态做一个比较,如果是第二次相同了,那么它是不会让组件重新渲染的,如果想深入研究的话可以去看一下react源码

总结

本篇文章就先讲到这里吧!下面来简单总结一下:

  1. 在正常的react的事件流里(如onClick等)

  • setState和useState中的set函数是异步执行的(不会立即更新state的结果)

  • 多次执行setState和useState的set函数,组件只会重新渲染一次

  • 不同的是,setState会更新当前作用域下的状态,但是set函数不会更新,只能在新渲染的组件作用域中访问到

  • 同时setState会进行state的合并,但是useState中的set函数做的操作相当于是直接替换,只不过内部有个防抖的优化才导致组件不会立即被重新渲染

  1. 在setTimeout,Promise.then等异步事件或者原生事件中

  • setState和useState的set函数是同步执行的(立即重新渲染组件)

  • 多次执行setState和useState的set函数,每一次的执行都会调用一次render


作者:Running53
链接:https://juejin.cn/post/7035104240217358350


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐