快照形式的状态

状态变量看起来可能像你可以读取和写入的普通 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) {
  // ...
}

以下是单击按钮时发生的情况

  1. onSubmit事件处理程序执行。
  2. setIsSent(true)isSent设置为true并排队新的渲染。
  3. React 根据新的isSent值重新渲染组件。

让我们仔细看看状态和渲染之间的关系。

渲染会获取时间快照

“渲染”意味着 React 正在调用你的组件(一个函数)。从该函数返回的 JSX 就像 UI 的时间快照。它的 props、事件处理程序和局部变量都是使用它在渲染时的状态计算的。

与照片或电影帧不同,你返回的 UI“快照”是交互式的。它包括像事件处理程序这样的逻辑,这些逻辑指定响应输入时会发生什么。React 更新屏幕以匹配此快照并连接事件处理程序。因此,按下按钮将触发你 JSX 中的点击处理程序。

当 React 重新渲染组件时

  1. React 再次调用你的函数。
  2. 你的函数返回一个新的 JSX 快照。
  3. 然后,React 更新屏幕以匹配你的函数返回的快照。
  1. React 执行函数
  2. 计算快照
  3. 更新 DOM 树

作为组件的内存,状态不像一个在函数返回后消失的普通变量。状态实际上“存在”于 React 本身——就像在架子上一样!——在你的函数之外。当 React 调用你的组件时,它会为你提供该特定渲染的状态快照。你的组件返回 UI 的快照,其 JSX 中包含一组新的 props 和事件处理程序,所有这些都是使用该渲染的状态值计算的!

  1. 你告诉 React 更新状态
  2. React 更新状态值
  3. React 将状态值的快照传递到组件中

这是一个小实验,向你展示它是如何工作的。在这个例子中,你可能期望点击“+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每次点击只递增一次!

设置状态只会更改_下一个_渲染的状态。在第一次渲染期间,number0。这就是为什么在_该渲染的_ onClick处理程序中,即使在调用setNumber(number + 1)之后,number的值仍然是0

<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>

以下是此按钮的点击处理程序告诉 React 要执行的操作

  1. setNumber(number + 1)number0,所以为 setNumber(0 + 1)
    • React 准备在下一次渲染中将 number 更改为 1
  2. setNumber(number + 1)number0,所以为 setNumber(0 + 1)
    • React 准备在下一次渲染中将 number 更改为 1
  3. setNumber(number + 1)number0,所以为 setNumber(0 + 1)
    • React 准备在下一次渲染中将 number 更改为 1

即使您调用了三次 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>

对于下一次渲染,number1,因此那次渲染的点击处理程序如下所示

<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 中,即使在调用 setNumber(number + 5) 之后,number 的值仍然为 0。当 React 通过调用您的组件“拍摄 UI 快照”时,其值被“固定”。

这是一个示例,说明这如何使您的事件处理程序不易出现计时错误。下面是一个带有五秒延迟的消息发送表单。想象一下这种情况

  1. 您按下“发送”按钮,将“你好”发送给爱丽丝。
  2. 在五秒延迟结束之前,您将“收件人”字段的值更改为“鲍勃”。

您期望alert显示什么?它会显示“你对爱丽丝说了你好”吗?还是会显示“你对鲍勃说了你好”?根据您的了解进行猜测,然后尝试一下

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 调用之前还是之后有区别吗?