**,setState到底是同步的还是异步的?

常规使用方法

在class组件中,通过setState的方式修改组件的状态:

示例一:
class App extends React.components{
    constructor(props){
        super(props)
    }
    state: {
        count: 0
    }
    handleClick(){
        this.setState({count: this.state.count+1})
    }

    render(){
        return (
            <>
                <Button onClick={this.handleClick.bind(this)}></Button>                <div>{count}</div>
            </>
            
        )
    }
}
复制代码

在函数式组件中,通过useState这个hook管理state状态:

示例二:
import React, { useState } from 'react'

const APP:React.FC = (props)=>{
    const [count, setCount] = useState(0)
    const handleClick = () => {
        setCount(count + 1) 
        console.log(count) // 拿到的是更新前的值0
    }

    return (
        <>
            <Button onClick={handleClick}></Button>    
            <div>{count}</div>
        </>
    )
}
复制代码

以上是两种使用方式的对比。大部分时候,以上的使用方式已经能够适应大部分的业务需求。但是,面试官可不这么想,而且你理解的这么简单,那么也可能在使用的过程中,不知不觉的写出bug。

接下来,我会慢慢的分析一下它们之间在使用时差异以及需要注意到的地方。

透过现象看本质

合并策略

将示例一的代码稍微改一改:

 handleClick(){
    this.setState({count:count+1})
    this.setState({count:count+1})
    this.setState({count:count+1})
    console.log(count) // 0 这里还没有拿到最新的值
}
复制代码

当点击按钮之后,以上代码执行完后,页面上最终显示的会是多少呢?答案是1。这是为什么?

原因:

react采用的是异步更新的策略,每执行一次setState都会开启一个异步任务到队列中去等待执行,以上3个stateState那就是3个异步任务。需要注意的是每一次异步任务开始执行的时候,都无法知道也不会关心其他异步任务的值,或者说,在异步任务创建的时候,这个count已经确定了是0,那么这3个setState异步任务传入的count的值都是0。

然后,react在渲染时,维护了一个更新队列并采取批量更新的方式,当多个异步任务进入队列后,它会对比它们之间的差异,只选择将最终的结果更新到页面上。

这样做也是为了性能,想象一下,如果每个异步任务都执行一次页面更新渲染,你将上面3个改成100个,那页面就需要渲染100次,这无疑是非常影响性能的。

同步还是异步?

通过上面的例子,可以发现,class组件中更新state貌似是异步的。先说答案,setState其实是同步的,但是在使用的时候,既有可能是同步的也有可能是“异步的”。

不过恕我直言,如果面试官问你的setState是同步还是异步的,你只回答一个异步或者同步,这都不是一个能让面试官合格的答案。

这到底是怎么回事?先来看一波例子:

class App extends React.components{
    constructor(props){
        super(props)
    }
    state: {
        count: 0
    }
    componentDidMount() {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 拿到的是更新前的值0
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)  // 拿到的是更新前的值0
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)  // 拿到的是更新前的值0
        setTimeout(() => {
            this.setState({ val: this.state.val + 1 })
            console.log(this.state.val) // 拿到的是更新后的值2
            this.setState({ val: this.state.val + 1 })
            console.log(this.state.val) // 拿到的是更新后的值3
        }, 0)
}

    render(){
        return (
            <>
                <Button onClick={this.handleClick.bind(this)}></Button> 
                <div>{count}</div>
            </>
        )
    }
}
复制代码

上面的例子中,在react的钩子函数componentDidMount中先执行了3个setState,然后在setTimeout延时中执行了2次,通过打印可以看到,在生命周期函数中,获取state的值是异步的;在setTimeout函数中,拿到的值是同步的。这是为什么?

原因:

1、class组件中,setState只在生命周期钩子函数及合成事件对应的方法中是“异步”的;

2、class组件中,setState在setTimeout异步方法及原生事件中是同步的。

3、其实,我想说的是,setState在生命周期钩子函数及合成事件也是同步更新count的值的,只是在钩子函数及合成事件的方法调用发生在react批量更新之前。所以拿到的依然是更新之前的count值,看起来像是异步。如果需要拿同步值,可以通过setState的callback。

有人可能会问,为什么会有这种现象,这里首先涉及到源码里面的设计,react针对生命周期函数合成事件和setTimeout原生事件设置了两种分支去执行更新策略。

函数式组件中的useState

解决了标题的疑问,再来看下在函数式组件中,useState的一些知识点吧。将示例二稍微改改:

示例二:
import React, { useState } from 'react'

const APP:React.FC = (props)=>{
    const [count, setCount] = useState(0)
    const handleClick = () => {
       setTimeout(()=> {
            setCount(count + 1)
        }, 3000)
    }

    return (
        <>
            <Button onClick={handleClick}></Button>    
            <div>{count}</div>
        </>
    )
}
复制代码

在3s内多次点击Button然后看 <div>{count}</div> 中显示的会是多少呢?答案是1。count并没有根据多次点击而多次累加,这是为什么?

假如我们把react函数式组件的更新想象成在放动画片,每更新一次就是一帧,那么,在某一次(某一帧)更新中,由于闭包的原因,它使用的状态都是那一次更新之前的状态值。即时你多次点击,在react没有完成更新前,它使用的都是更新前的值为0的那个count。

我还是引用一下官网的解释叭:

为什么我会在我的函数中看到陈旧的 props 和 state ?

如果你要避开这种问题,你可以稍微改一下写法

    const handleClick = () => {
       setTimeout(()=> {
            -- setCount(count + 1)
            ++ setCount(count => count + 1)
        }, 3000)
    }
复制代码

这样每次点击都会更新到最新的值。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享