状态变量可能看起来像可以读取和写入的常规 JavaScript 变量。但是,状态的行为更像快照。设置它不会更改你已有的状态变量,而是触发重新渲染。
你将学习
- 设置状态如何触发重新渲染
- 何时以及如何更新状态
- 为什么状态在你设置后不会立即更新
- 事件处理程序如何访问状态的“快照”
设置状态会触发渲染
你可能认为用户界面会像点击一样直接响应用户事件而发生变化。在 React 中,它的工作方式与此心理模型略有不同。在上一页中,你看到了设置状态会向 React 请求重新渲染。这意味着要使界面响应事件,你需要更新状态。
在这个例子中,当你按下“发送”时,setIsSent(true)
会告诉 React 重新渲染 UI
import { useState } from 'react'; export default function Form() { const [isSent, setIsSent] = useState(false); const [message, setMessage] = useState('Hi!'); if (isSent) { return <h1>Your message is on its way!</h1> } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); sendMessage(message); }}> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); } function sendMessage(message) { // ... }
以下是当你点击按钮时会发生的情况
- 会执行
onSubmit
事件处理程序。 setIsSent(true)
会将isSent
设置为true
并将新的渲染排入队列。- React 会根据新的
isSent
值重新渲染组件。
让我们仔细看看状态和渲染之间的关系。
渲染会捕捉某个时间点的快照
“渲染” 意味着 React 正在调用你的组件,它是一个函数。你从该函数返回的 JSX 就像某个时间点 UI 的快照。它的 props、事件处理程序和局部变量都是 使用渲染时的状态 计算出来的。
与照片或电影帧不同,你返回的 UI“快照”是交互式的。它包含逻辑,例如指定响应输入时会发生什么的事件处理程序。React 会更新屏幕以匹配此快照并连接事件处理程序。因此,按下按钮将触发 JSX 中的点击处理程序。
当 React 重新渲染组件时
- React 会再次调用你的函数。
- 你的函数会返回一个新的 JSX 快照。
- 然后,React 会更新屏幕以匹配你的函数返回的快照。
插图作者 Rachel Lee Nabors
作为组件的内存,状态不像函数返回后就会消失的常规变量。状态实际上“存在”于 React 本身——就像在架子上!——在你的函数之外。当 React 调用你的组件时,它会为你提供该特定渲染的状态快照。你的组件会在其 JSX 中返回一个 UI 快照,其中包含一组新的 props 和事件处理程序,所有这些都是 使用该渲染中的状态值 计算出来的!
插图作者 Rachel Lee Nabors
这里有一个小实验可以向你展示它是如何工作的。在这个例子中,你可能期望点击“+3”按钮会使计数器递增三次,因为它调用了三次 setNumber(number + 1)
。
看看当你点击“+3”按钮时会发生什么
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> </> ) }
注意 number
每次点击只递增一次!
设置状态只会在下一次渲染时更改它。在第一次渲染期间,number
为 0
。这就是为什么在该渲染的 onClick
处理程序中,即使在调用了 setNumber(number + 1)
之后,number
的值仍然为 0
<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
,所以你三次将状态设置为 1
。这就是为什么在你的事件处理程序完成后,React 会使用 number
等于 1
而不是 3
重新渲染组件的原因。
你也可以通过在代码中用状态变量的值来替换它们,从而在脑海中将它们可视化。由于*此渲染*的 number
状态变量为 0
,因此它的事件处理程序如下所示
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
对于下一次渲染,number
为 1
,因此*该渲染*的点击处理程序如下所示
<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>
这就是为什么再次点击按钮会将计数器设置为 2
,然后在下一次点击时设置为 3
,以此类推。
状态随时间变化
很好,这很有趣。试着猜猜点击这个按钮会弹出什么
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”
setNumber(0 + 5);
alert(0);
但是,如果你为弹出窗口设置一个计时器,让它只在组件重新渲染*之后*才触发呢?它会显示 “0” 还是 “5”?猜猜看!
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> </> ) }
惊讶吗?如果你使用替换方法,你可以看到传递给弹出窗口的状态 “快照”。
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
存储在 React 中的状态在弹出窗口运行时可能已经改变,但是它是使用用户与之交互时的状态快照来调度的!
状态变量的值在一次渲染中永远不会改变,即使它的事件处理程序代码是异步的。在*该渲染*的 onClick
内部,number
的值在调用 setNumber(number + 5)
之后仍然是 0
。当 React 通过调用你的组件 “获取 UI 的快照” 时,它的值就被 “固定” 了。
下面是一个例子,说明这如何使你的事件处理程序更不容易出现计时错误。下面是一个以五秒钟的延迟发送消息的表单。想象一下这种情况
- 你按下 “发送” 按钮,向 Alice 发送 “你好”。
- 在五秒钟的延迟结束之前,你将 “收件人” 字段的值更改为 “Bob”。
你希望 alert
显示什么?它会显示 “你对 Alice 说你好”?还是会显示 “你对 Bob 说你好”?根据你所知道的猜一猜,然后试一试
import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Hello'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`You said ${message} to ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> To:{' '} <select value={to} onChange={e => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); }
React 在一次渲染的事件处理程序中保持状态值 “固定”。你不需要担心代码运行时状态是否已更改。
但是,如果你想在重新渲染之前读取最新的状态呢?你需要使用一个状态更新函数,这将在下一页介绍!
回顾
- 设置状态会请求一个新的渲染。
- React 将状态存储在组件外部,就像在架子上一样。
- 当你调用
useState
时,React 会为你提供*该渲染*的状态快照。 - 变量和事件处理程序不会 “在重新渲染后保留”。每次渲染都有自己的事件处理程序。
- 每次渲染(及其内部的函数)将始终 “看到” React 提供给*该*渲染的状态快照。
- 你可以在事件处理程序中用状态值替换它们,就像你思考渲染的 JSX 一样。
- 过去创建的事件处理程序具有创建它们时的渲染的状态值。
挑战 1共 1: 实现一个交通信号灯
这是一个在按下按钮时切换的人行交通信号灯组件
import { useState } from 'react'; export default function TrafficLight() { const [walk, setWalk] = useState(true); function handleClick() { setWalk(!walk); } return ( <> <button onClick={handleClick}> Change to {walk ? 'Stop' : 'Walk'} </button> <h1 style={{ color: walk ? 'darkgreen' : 'darkred' }}> {walk ? 'Walk' : 'Stop'} </h1> </> ); }
向点击处理程序添加一个 alert
。当灯为绿色并显示 “行走” 时,点击按钮应显示 “下一个是停止”。当灯为红色并显示 “停止” 时,点击按钮应显示 “下一个是行走”。
将 alert
放在 setWalk
调用之前还是之后有区别吗?