参考文章
也许 state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照。设置它不会更改已有的 state 变量,但会触发重新渲染。
可能会认为用户界面会直接对点击之类的用户输入做出响应并发生变化。在 React 中,它的工作方式与这种思维模型略有不同。这意味着要使界面对输入做出反应,需要设置其 state。
在这个例子中,当按下 “send” 时,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
并排列一个新的渲染。isSent
值重新渲染组件。仔细看看 state 和渲染之间的关系。
“正在渲染” 就意味着 React 正在调用组件——一个函数。从该函数返回的 JSX 就像是 UI 的一张及时的快照。它的 props、事件处理函数和内部变量都是 根据当前渲染时的 state 被计算出来的。
与照片或电影画面不同,返回的 UI “快照”是可交互的。它其中包括类似事件处理函数的逻辑,这些逻辑用于指定如何对输入作出响应。React 随后会更新屏幕来匹配这张快照,并绑定事件处理函数。因此,按下按钮就会触发JSX 中的点击事件处理函数。
当 React 重新渲染一个组件时:
React 执行函数 ——> 计算快照 ——> 更新 DOM 树
作为一个组件的记忆(状态),state 不同于在函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于函数之外。当 React 调用组件时,它会为特定的那一次渲染提供一张 state 快照。组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!
React 收到 setUpdate 通知 ——> React 更新 state 的值 ——> React 向组件内传入一张 state 的快照
这里有个展示其运行原理的小例子。在这个例子中,可能会以为点击“+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
递增一次!
设置 state 只会为下一次渲染变更 state 的值。在第一次渲染期间,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)
。
number
更改为 1
。setNumber(number + 1)
: number
是 0
,所以 setNumber(0 + 1)
。
number
更改为 1
。setNumber(number + 1)
: number
是 0
,所以 setNumber(0 + 1)
。
number
更改为 1
。尽管调用了三次 setNumber(number + 1)
,但在 这次渲染的 事件处理函数中 number
会一直是 0
,所以会三次将 state 设置成 1
。这就是为什么在事件处理函数执行完以后,React 重新渲染的组件中的 number
等于 1
而不是 3
。
还可以通过在心里把 state 变量替换成它们在你代码中的值来想象这个过程。由于 这次渲染 中的 state 变量 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” ?
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
如果使用替代法,就能看到被传入提示框的 state “快照”。答案是:显示 “0”。
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
到提示框运行时,React 中存储的 state 可能已经发生了更改,但它是使用用户与之交互时状态的快照进行调度的!
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。在 那次渲染的 onClick
内部,number
的值即使在调用 setNumber(number + 5)
之后也还是 0
。它的值在 React 通过调用组件“获取 UI 的快照”时就被“固定”了。
这里有个示例能够说明上述特性会使事件处理函数更不容易出现计时错误。下面是一个会在五秒延迟之后发送一条消息的表单。想象以下场景:
你觉得 alert
会显示什么?它是会显示“你向 Alice 说了你好“还是会显示“你向 Bob 说了你好”?
答案是:会显示“你向 Alice 说了你好“
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 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。 无需担心代码运行时 state 是否发生了变化。
但是,万一想在重新渲染之前读取最新的 state 怎么办?应该使用 状态更新函数。
useState
时,React 会为提供该次渲染 的一张 state 快照。