状态如同快照
状态变量可能看起来像可以读取和写入的普通 JavaScript 变量。然而,状态的行为更像一个快照。设置它并不会改变你已有的状态变量,而是会触发一次重新渲染。
您将学习
- 设置状态如何触发重新渲染
- 状态何时以及如何更新
- 为什么设置状态后状态不会立即更新
- 事件处理程序如何访问状态的“快照”
设置状态会触发渲染
你可能会认为用户界面会直接响应用户事件(如点击)而改变。在 React 中,它的工作方式与这种心智模型略有不同。在上一页,你看到设置状态会请求一次重新渲染。这意味着要让界面响应事件,你需要更新状态。
在这个例子中,当你按下“发送”时,setIsSent(true)会告诉 React 重新渲染 UI:
当你点击按钮时会发生以下事情:
- 执行
onSubmit事件处理程序。 setIsSent(true)将isSent设置为true并排队等待一次新的渲染。- React 根据新的
isSent值重新渲染组件。
让我们更仔细地看看状态和渲染之间的关系。
渲染会及时拍摄快照
“渲染”意味着 React 正在调用你的组件,它是一个函数。你从该函数返回的 JSX 就像是 UI 在某个时间点的快照。它的 props、事件处理程序和局部变量都是根据渲染时的状态计算出来的。
与照片或电影帧不同,您返回的 UI“快照”是交互式的。它包含了诸如事件处理器之类的逻辑,这些逻辑指定了如何响应输入。React 会更新屏幕以匹配此快照并连接事件处理器。因此,按下按钮将触发您 JSX 中的点击处理器。
当 React 重新渲染组件时:
- React 会再次调用您的函数。
- 您的函数返回一个新的 JSX 快照。
- 然后 React 更新屏幕以匹配您的函数返回的快照。
React 执行函数
计算快照
更新 DOM 树
作为组件的记忆,状态不像函数返回后就消失的普通变量。状态实际上“存活”在 React 本身中——就像放在架子上一样!——在您的函数之外。当 React 调用您的组件时,它会为您提供该特定渲染的状态快照。您的组件在其 JSX 中返回一个带有新 props 和事件处理器的 UI 快照,所有这些计算都是使用该次渲染的状态值进行的!
您告诉 React 更新状态
React 更新状态值
React 将状态值的快照传递给组件
这里有一个小实验向您展示这是如何工作的。在这个例子中,您可能期望点击“+3”按钮会使计数器增加三次,因为它调用了setNumber(number + 1)三次。
看看当您点击“+3”按钮时会发生什么:
请注意,number每次点击只增加一次!
设置状态只会改变下一次渲染的状态。在第一次渲染期间,number是0。这就是为什么,在那次渲染的onClick处理函数中,number的值即使在调用了setNumber(number + 1)之后仍然是0:
以下是这个按钮的点击处理函数告诉 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,因此你将状态设置为1三次。这就是为什么在你的事件处理函数执行完毕后,React 重新渲染组件时number 等于 1 而不是 3。
你也可以通过在脑海中将状态变量替换为它们在代码中的值来形象化地理解这一点。由于number 状态变量在0,因此其事件处理函数看起来像这样:本次渲染中是
对于下一次渲染,number 是 1,所以该次渲染的点击处理函数看起来是这样的:
这就是为什么再次点击按钮会将计数器设置为2,然后在下次点击时设置为3,依此类推。
随时间变化的状态
嗯,这很有趣。试着猜猜点击这个按钮会弹出什么:
如果使用之前的替换方法,你可以猜到警告框会显示“0”:
但是,如果你给警告框加上一个定时器,让它只在组件重新渲染之后才触发呢?它会显示“0”还是“5”?猜猜看!
感到意外吗?如果使用替换方法,你可以看到传递给警告框的状态“快照”。
当警告框运行时,存储在 React 中的状态可能已经改变,但它是使用用户与之交互时的状态快照来调度的!
状态变量的值在一次渲染中永远不会改变,即使其事件处理函数的代码是异步的。在那次渲染的onClick内部,number的值始终是0,即使在调用setNumber(number + 5)之后也是如此。当 React 通过调用你的组件来“拍摄” UI 的快照时,它的值就被“固定”了。
下面是一个例子,说明这如何使你的事件处理函数更不容易出现时序错误。下面是一个带有五秒延迟发送消息的表单。想象一下这个场景:
- 你按下“发送”按钮,向 Alice 发送“Hello”。
- 在五秒延迟结束之前,你将“收件人”字段的值更改为“Bob”。
你期望alert显示什么?它会显示“你对 Alice 说了 Hello”吗?还是会显示“你对 Bob 说了 Hello”?根据你所知道的猜一猜,然后试试看:
React 在一次渲染的事件处理函数中保持状态值“固定”。你无需担心代码运行时状态是否已改变。
但是,如果你想在重新渲染之前读取最新的状态呢?你将需要使用状态更新函数,这将在下一页介绍!
回顾
- 设置状态会请求一次新的渲染。
- React 将状态存储在你的组件之外,就像放在架子上一样。
- 当你调用
useState时,React 会为你提供该次渲染的状态快照。 - 变量和事件处理函数不会“存活”于重新渲染。每次渲染都有自己的事件处理函数。
- 每次渲染(及其内部的函数)将始终“看到”React 提供给该次渲染的状态快照。
- 你可以在脑海中替换事件处理函数中的状态,类似于你思考渲染出的 JSX 的方式。
- 过去创建的事件处理函数拥有它们被创建时那次渲染的状态值。
尝试一些挑战
Challenge 1 of 1:实现一个交通信号灯 #
这是一个行人信号灯组件,按下按钮时会切换状态:
在点击处理函数中添加一个 alert。当信号灯为绿色并显示“Walk”时,点击按钮应提示“Stop is next”。当信号灯为红色并显示“Stop”时,点击按钮应提示“Walk is next”。
将 alert 放在 setWalk 调用之前或之后有区别吗?
