状态变量可能看起来像你可以读取和写入的常规 JavaScript 变量。但是,状态的行为更像是快照。设置它不会更改你已有的状态变量,而是会触发重新渲染。

¥State variables might look like regular JavaScript variables that you can read and write to. However, state behaves more like a snapshot. Setting it does not change the state variable you already have, but instead triggers a re-render.

你将学习到

  • 设置状态如何触发重新渲染

    ¥How setting state triggers re-renders

  • 状态何时以及如何更新

    ¥When and how state updates

  • 为什么设置后状态不会立即更新

    ¥Why state does not update immediately after you set it

  • 事件处理程序如何访问状态的 “快照”

    ¥How event handlers access a “snapshot” of the state

设置状态触发渲染

¥Setting state triggers renders

你可能认为你的用户界面会直接响应用户事件(如点击)而发生变化。在 React 中,它的工作方式与这种心智模型略有不同。在上一页中,你看到了来自 React 的 设置状态请求重新渲染。这意味着要让界面对事件做出 React,你需要更新状态。

¥You might think of your user interface as changing directly in response to the user event like a click. In React, it works a little differently from this mental model. On the previous page, you saw that setting state requests a re-render from React. This means that for an interface to react to the event, you need to update the state.

在这个例子中,当你按下 “发送” 时,setIsSent(true) 告诉 React 重新渲染 UI:

¥In this example, when you press “send”, setIsSent(true) tells React to re-render the 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) {
  // ...
}

单击按钮时会发生以下情况:

¥Here’s what happens when you click the button:

  1. onSubmit 事件处理程序执行。

    ¥The onSubmit event handler executes.

  2. setIsSent(true)isSent 设置为 true 并将新渲染排队。

    ¥setIsSent(true) sets isSent to true and queues a new render.

  3. React 根据新的 isSent 值重新渲染组件。

    ¥React re-renders the component according to the new isSent value.

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

¥Let’s take a closer look at the relationship between state and rendering.

渲染及时拍快照

¥Rendering takes a snapshot in time

”渲染” 表示 React 正在调用你的组件,这是一个函数。从该函数返回的 JSX 就像 UI 的及时快照。它的属性、事件处理程序和局部变量都是使用渲染时的状态来计算的。

¥“Rendering” means that React is calling your component, which is a function. The JSX you return from that function is like a snapshot of the UI in time. Its props, event handlers, and local variables were all calculated using its state at the time of the render.

与照片或电影画面不同,你返回的 UI “快照” 是交互式的。它包括类似事件处理程序的逻辑,用于指定响应输入时发生的事情。React 更新屏幕以匹配此快照并连接事件处理程序。因此,按下按钮将触发来自 JSX 的点击处理程序。

¥Unlike a photograph or a movie frame, the UI “snapshot” you return is interactive. It includes logic like event handlers that specify what happens in response to inputs. React updates the screen to match this snapshot and connects the event handlers. As a result, pressing a button will trigger the click handler from your JSX.

当 React 重新渲染一个组件时:

¥When React re-renders a component:

  1. React 再次调用你的函数。

    ¥React calls your function again.

  2. 你的函数返回一个新的 JSX 快照。

    ¥Your function returns a new JSX snapshot.

  3. 然后 React 更新屏幕以匹配函数返回的快照。

    ¥React then updates the screen to match the snapshot your function returned.

  1. React executing the function
  2. Calculating the snapshot
  3. Updating the DOM tree

Illustrated by Rachel Lee Nabors

作为组件的内存,状态不像一个常规变量,在你的函数返回后就消失了。实际上在 React 本身中声明 “存在” - 就像束之高阁一样! - 在你的函数之外。当 React 调用你的组件时,它会为你提供该特定渲染的状态快照。你的组件返回 UI 的快照,并在其 JSX 中包含一组新的属性和事件处理程序,所有这些都使用该渲染中的状态值进行计算!

¥As a component’s memory, state is not like a regular variable that disappears after your function returns. State actually “lives” in React itself—as if on a shelf!—outside of your function. When React calls your component, it gives you a snapshot of the state for that particular render. Your component returns a snapshot of the UI with a fresh set of props and event handlers in its JSX, all calculated using the state values from that render!

  1. You tell React to update the state
  2. React updates the state value
  3. React passes a snapshot of the state value into the component

Illustrated by Rachel Lee Nabors

这是一个小实验,向你展示这是如何工作的。在此示例中,你可能希望单击 “+3” 按钮会使计数器递增 3 次,因为它调用了 3 次 setNumber(number + 1)

¥Here’s a little experiment to show you how this works. In this example, you might expect that clicking the “+3” button would increment the counter three times because it calls setNumber(number + 1) three times.

看看当你点击 “+3” 按钮时会发生什么:

¥See what happens when you click the “+3” button:

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 每次点击只会增加一次!

¥Notice that number only increments once per click!

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

¥Setting state only changes it for the next render. During the first render, number was 0. This is why, in that render’s onClick handler, the value of number is still 0 even after setNumber(number + 1) was called:

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

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

¥Here is what this button’s click handler tells React to do:

  1. setNumber(number + 1)number0 所以 setNumber(0 + 1)

    ¥setNumber(number + 1): number is 0 so setNumber(0 + 1).

    • React 准备在下一次渲染时将 number 更改为 1

      ¥React prepares to change number to 1 on the next render.

  2. setNumber(number + 1)number0 所以 setNumber(0 + 1)

    ¥setNumber(number + 1): number is 0 so setNumber(0 + 1).

    • React 准备在下一次渲染时将 number 更改为 1

      ¥React prepares to change number to 1 on the next render.

  3. setNumber(number + 1)number0 所以 setNumber(0 + 1)

    ¥setNumber(number + 1): number is 0 so setNumber(0 + 1).

    • React 准备在下一次渲染时将 number 更改为 1

      ¥React prepares to change number to 1 on the next render.

即使你调用了 setNumber(number + 1) 三次,在此渲染的事件处理程序中,number 始终是 0,因此你将状态设置为 1 三次。这就是为什么在事件处理程序完成后,React 会重新渲染 number 等于 1 而不是 3 的组件。

¥Even though you called setNumber(number + 1) three times, in this render’s event handler number is always 0, so you set the state to 1 three times. This is why, after your event handler finishes, React re-renders the component with number equal to 1 rather than 3.

你还可以通过在代码中用状态变量的值在心里替换它们来形象化这一点。由于此渲染的 number 状态变量为 0,因此其事件处理程序如下所示:

¥You can also visualize this by mentally substituting state variables with their values in your code. Since the number state variable is 0 for this render, its event handler looks like this:

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

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

¥For the next render, number is 1, so that render’s click handler looks like this:

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

这就是为什么再次单击该按钮会将计数器设置为 2,然后在下一次单击时设置为 3,依此类推。

¥This is why clicking the button again will set the counter to 2, then to 3 on the next click, and so on.

随时间变化的状态

¥State over time

好吧,那很有趣。尝试猜测点击这个按钮会提示什么:

¥Well, that was fun. Try to guess what clicking this button will alert:

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”:

¥If you use the substitution method from before, you can guess that the alert shows “0”:

setNumber(0 + 5);
alert(0);

但是,如果你将计时器置于警报状态,使其仅在组件重新渲染后触发怎么办?它会说 “0” 还是 “5”?猜一猜!

¥But what if you put a timer on the alert, so it only fires after the component re-rendered? Would it say “0” or “5”? Have a guess!

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>
    </>
  )
}

惊讶吗?如果使用替换方法,你可以看到传递给警报的状态的 “快照”。

¥Surprised? If you use the substitution method, you can see the “snapshot” of the state passed to the alert.

setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);

警报运行时存储在 React 中的状态可能已经改变,但它是使用用户与之交互时的状态快照来安排的!

¥The state stored in React may have changed by the time the alert runs, but it was scheduled using a snapshot of the state at the time the user interacted with it!

状态变量的值在渲染中永远不会改变,即使其事件处理程序的代码是异步的。在该渲染器的 onClick 中,即使在调用 setNumber(number + 5) 之后,number 的值仍然是 0。当通过调用你的组件对 UI 进行 React “拍了快照” 时,它的值为 “固定”。

¥A state variable’s value never changes within a render, even if its event handler’s code is asynchronous. Inside that render’s onClick, the value of number continues to be 0 even after setNumber(number + 5) was called. Its value was “fixed” when React “took the snapshot” of the UI by calling your component.

下面是一个示例,说明这如何使你的事件处理程序更不容易出现计时错误。下面是一个以五秒延迟发送消息的表单。想象一下这个场景:

¥Here is an example of how that makes your event handlers less prone to timing mistakes. Below is a form that sends a message with a five-second delay. Imagine this scenario:

  1. 你按下 “发送” 按钮,将 “你好” 发送给 Alice。

    ¥You press the “Send” button, sending “Hello” to Alice.

  2. 在五秒延迟结束之前,你将 “到达” 字段的值更改为 “Bob”。

    ¥Before the five-second delay ends, you change the value of the “To” field to “Bob”.

你期望 alert 显示什么?它会显示 “你向 Alice 问好” 吗?或者它会显示,“你向 Bob 问好”?根据你所知道的进行猜测,然后尝试:

¥What do you expect the alert to display? Would it display, “You said Hello to Alice”? Or would it display, “You said Hello to Bob”? Make a guess based on what you know, and then try it:

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 keeps the state values “fixed” within one render’s event handlers. You don’t need to worry whether the state has changed while the code is running.

但是如果你想在重新渲染之前读取最新状态怎么办?你会想要使用 状态更新函数,将在下一页介绍!

¥But what if you wanted to read the latest state before a re-render? You’ll want to use a state updater function, covered on the next page!

回顾

  • 设置状态请求新的渲染。

    ¥Setting state requests a new render.

  • React 将状态存储在组件外部,就像束之高阁一样。

    ¥React stores state outside of your component, as if on a shelf.

  • 当你调用 useState 时,React 会为你提供该渲染状态的快照。

    ¥When you call useState, React gives you a snapshot of the state for that render.

  • 变量和事件处理程序在重新渲染时不会 “幸存”。每个渲染器都有自己的事件处理程序。

    ¥Variables and event handlers don’t “survive” re-renders. Every render has its own event handlers.

  • 每个渲染器(以及其中的函数)都将始终 “看到” React 提供给该渲染器的状态快照。

    ¥Every render (and functions inside it) will always “see” the snapshot of the state that React gave to that render.

  • 你可以在心理上替换事件处理程序中的状态,类似于你对渲染的 JSX 的看法。

    ¥You can mentally substitute state in event handlers, similarly to how you think about the rendered JSX.

  • 过去创建的事件处理程序具有创建它们时所在的渲染中的状态值。

    ¥Event handlers created in the past have the state values from the render in which they were created.

挑战 1 / 1:
实现红绿灯

¥Implement a traffic light

这是一个在按下按钮时切换的人行横道灯组件:

¥Here is a crosswalk light component that toggles when the button is pressed:

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 添加到点击处理程序。当指示灯为绿色并显示 “行走” 时,单击按钮应显示 “下一个是停止”。当指示灯为红色并显示 “停止” 时,单击按钮应显示 “下一个是行走”。

¥Add an alert to the click handler. When the light is green and says “Walk”, clicking the button should say “Stop is next”. When the light is red and says “Stop”, clicking the button should say “Walk is next”.

alert 放在 setWalk 调用之前或之后会有什么不同吗?

¥Does it make a difference whether you put the alert before or after the setWalk call?


React 中文网 - 粤ICP备13048890号