state和useState是react中很重要的概念,虽然笔者一直在用,但是总感觉有些地方认识不够透彻。于是乎,笔者重新阅读学习了react官方文档,感觉受益匪浅。希望能用尽量通俗简洁的语言把吸收的知识表述清楚,便写下此文。
如有错误,欢迎指正~
目录
state
普通变量
当我们需要数据响应式渲染到页面上时,我们就不能使用普通的变量了:
- 普通变量无法在多次渲染中持久保存:当 React 再次渲染这个组件时,它会从头开始渲染——不会考虑之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
此时,我们就需要useState来声明state去解决上面的问题。
深入了解state
官网中说到:“State是隔离且私有的”,那么这句话应该如何理解呢?
隔离:State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
私有:与 props 不同,state 完全私有于<声明>它的组件。父组件无法更改它。
//隔离:son组件渲染两次,两个son组件中的state互相隔离,互不影响
//私有:parent不知道son组件中的state的任何信息,无法改变son组件的state(符合react的单项数据流动特性)
<parent>
<son/>
<son/>
</parent>
useState
基本用法
useState 返回一个由两个值组成的数组:
当前的 state:在首次渲染时,它将与你传递的 initialState 相匹配。
set 函数:它可以让你将 state 更新为不同的值并《触发重新渲染》。
const [state, setState] = useState(initialState);
set函数触发重新渲染
在react中,触发组件渲染有两种情况:
- 组件的 初次渲染:调用render函数
- 组件(或者其祖先之一)的 状态发生了改变:调用set函数
在使用set函数之后,react会从该state声明(useState)的组件开始,根据更改后的state,重新渲染该组件&所有子组件。
*state--快照(重要!)
为了更好的理解,我们可以把state理解为一张快照:当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的。
为了更好的理解,我们接下来看几个例子
例一
该例子是一个简易计数器,click事件里面调用了三次setNumber,当我们点击按钮之后会发生什么呢?
//App.js
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
点击之后发现,每次点击值只增加1,而非3。这是为什么呢?
我们来捋一下:
step1:该组件第一次渲染时,number的值是0,所以后续的渲染都是根据0这个值进行的计算。
step2:第一次组件渲染完成后,我们看下的onClick函数:
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
以下是这个按钮的点击事件处理函数通知 React 要做的事情:
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
尽管你调用了三次 setNumber(number + 1)
,但在 这次渲染的 事件处理函数中 number
会一直是 0
,所以你会三次将 state 设置成 1
。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number
等于 1
而不是 3
。
所以我们在第一次渲染之后,onClick函数可以等价为如下代码:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
例二
我们把上面的例子改一下,再预测一下会出现什么结果:
//App.js
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
点击之后发现:数字从0变成5了,但是弹窗的数依旧是0。
根据我们上面提到的快照理解,上面的onClick代码可以等价为:
<button onClick={() => {
setNumber(0 + 5);
alert(0);
}}>+5</button>
这样是不是就很好理解了。
例三
我们再改一下:如果我们加一个定时器,想让弹窗在组件重新渲染 之后 才触发,又会怎样呢?
//App.js
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
结果: 数字从0变成5了,但是三秒之后的弹窗的数依旧是0。
这是为什么呢?我们看react官方如何解释:
“一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。”
“React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。”
state作为快照,它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了,哪怕是异步代码,它得到的state值也是在调用时被固定的快照的值。
所以对应函数等价于如下代码:
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
例四:更新函数
让我们回到例一,如果我们想在onClick里面写三个set函数来实现加3的效果,那该如何改造呢?
这时候就需要用到更新函数了:在set函数中,传入一个根据队列中的前一个 state 计算下一个 state 的 函数。
简单来说,就是把set函数的参数,从之前的一个值/变量,换成一个函数即可。
更改后的代码见下图:
这样,每点击一次,加的就是3了。
详细更新过程见下图:
例五
我们再升级一下例4的代码,运行结果会怎样呢?
结果:点击按钮后,从0变成了42。
详细更新过程见下图:
总结
- 调用set函数会请求一次新的渲染
- 每个渲染(以及其中的函数)始终“看到”的是 React 提供给这个 渲染的 state 快照
- 过去创建的事件处理函数拥有的是创建它们的那次渲染中的 state 值。
- React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。
- 要在一个事件中多次更新某些 state,你可以使用
setNumber(n => n + 1)
更新函数。