常规使用方法
在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)
}
复制代码
这样每次点击都会更新到最新的值。