diff --git a/src/content/learn/synchronizing-with-effects.md b/src/content/learn/synchronizing-with-effects.md
index 1641ef7b9c..fd4a321025 100644
--- a/src/content/learn/synchronizing-with-effects.md
+++ b/src/content/learn/synchronizing-with-effects.md
@@ -1,97 +1,97 @@
---
-title: 'Synchronizing with Effects'
+title: '与 Effect 同步'
---
-Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. *Effects* let you run some code after rendering so that you can synchronize your component with some system outside of React.
+有些组件需要与外部系统同步。例如,你可能希望根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。
-- What Effects are
-- How Effects are different from events
-- How to declare an Effect in your component
-- How to skip re-running an Effect unnecessarily
-- Why Effects run twice in development and how to fix them
+- 什么是 Effect
+- Effect 与事件(event)有何不同
+- 如何在组件中声明 Effect
+- 如何避免不必要地重新运行 Effect
+- 为什么 Effect 在开发环境中会影响两次,如何修复它们
-## What are Effects and how are they different from events? {/*what-are-effects-and-how-are-they-different-from-events*/}
+## 什么是 Effect,它与事件(event)有何不同? {/*what-are-effects-and-how-are-they-different-from-events*/}
-Before getting to Effects, you need to be familiar with two types of logic inside React components:
+在谈到 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:
-- **Rendering code** (introduced in [Describing the UI](/learn/describing-the-ui)) lives at the top level of your component. This is where you take the props and state, transform them, and return the JSX you want to see on the screen. [Rendering code must be pure.](/learn/keeping-components-pure) Like a math formula, it should only _calculate_ the result, but not do anything else.
+- **渲染逻辑代码**(在 [描述 UI](/learn/describing-the-ui) 中有介绍)位于组件的顶层。你将在这里接收 props 和 state,并对它们进行转换,最终返回你想在屏幕上看到的 JSX。[渲染的代码必须是纯粹的](/learn/keeping-components-pure)——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。
-- **Event handlers** (introduced in [Adding Interactivity](/learn/adding-interactivity)) are nested functions inside your components that *do* things rather than just calculate them. An event handler might update an input field, submit an HTTP POST request to buy a product, or navigate the user to another screen. Event handlers contain ["side effects"](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) (they change the program's state) caused by a specific user action (for example, a button click or typing).
+- **事件处理程序**(在 [添加交互性](/learn/adding-interactivity) 中介绍)是嵌套在组件内部的函数,而不仅仅是计算函数。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”(它们改变了程序的状态)。
-Sometimes this isn't enough. Consider a `ChatRoom` component that must connect to the chat server whenever it's visible on the screen. Connecting to a server is not a pure calculation (it's a side effect) so it can't happen during rendering. However, there is no single particular event like a click that causes `ChatRoom` to be displayed.
+有时这还不够。考虑一个 `ChatRoom` 组件,它在屏幕上可见时必须连接到聊天服务器。连接到服务器不是一个纯计算(它包含副作用),因此它不能在渲染过程中发生。然而,并没有一个特定的事件(比如点击)导致 `ChatRoom` 被显示。
-***Effects* let you specify side effects that are caused by rendering itself, rather than by a particular event.** Sending a message in the chat is an *event* because it is directly caused by the user clicking a specific button. However, setting up a server connection is an *Effect* because it should happen no matter which interaction caused the component to appear. Effects run at the end of a [commit](/learn/render-and-commit) after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).
+**Effect 允许你指定由渲染本身,而不是特定事件引起的副作用**。在聊天中发送消息是一个“事件”,因为它直接由用户点击特定按钮引起。然而,建立服务器连接是 Effect,因为它应该发生无论哪种交互导致组件出现。Effect 在屏幕更新后的 [提交阶段](/learn/render-and-commit) 运行。这是一个很好的时机,可以将 React 组件与某个外部系统(如网络或第三方库)同步。
-Here and later in this text, capitalized "Effect" refers to the React-specific definition above, i.e. a side effect caused by rendering. To refer to the broader programming concept, we'll say "side effect".
+在本文和后续文本中,`Effect` 在 React 中是专有定义——由渲染引起的副作用。为了指代更广泛的编程概念,也可以将其称为“副作用(side effect)”。
-## You might not need an Effect {/*you-might-not-need-an-effect*/}
+## 你可能不需要 Effect {/*you-might-not-need-an-effect*/}
-**Don't rush to add Effects to your components.** Keep in mind that Effects are typically used to "step out" of your React code and synchronize with some *external* system. This includes browser APIs, third-party widgets, network, and so on. If your Effect only adjusts some state based on other state, [you might not need an Effect.](/learn/you-might-not-need-an-effect)
+**不要随意在你的组件中使用 Effect**。记住,Effect 通常用于暂时“跳出” React 代码并与一些 **外部** 系统进行同步。这包括浏览器 API、第三方小部件,以及网络等等。如果你想用 Effect 仅根据其他状态调整某些状态,那么 [你可能不需要 Effect](/learn/you-might-not-need-an-effect)。
-## How to write an Effect {/*how-to-write-an-effect*/}
+## 如何编写 Effect {/*how-to-write-an-effect*/}
-To write an Effect, follow these three steps:
+编写 Effect 需要遵循以下三个规则:
-1. **Declare an Effect.** By default, your Effect will run after every render.
-2. **Specify the Effect dependencies.** Most Effects should only re-run *when needed* rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes. You will learn how to control this by specifying *dependencies.*
-3. **Add cleanup if needed.** Some Effects need to specify how to stop, undo, or clean up whatever they were doing. For example, "connect" needs "disconnect", "subscribe" needs "unsubscribe", and "fetch" needs either "cancel" or "ignore". You will learn how to do this by returning a *cleanup function*.
+1. **声明 Effect**。默认情况下,Effect 会在每次渲染后都会执行。
+2. **指定 Effect 依赖**。大多数 Effect 应该按需执行,而不是在每次渲染后都执行。例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。文章将介绍如何通过指定依赖来控制如何按需执行。
+3. **必要时添加清理(cleanup)函数**。有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。你将学习如何使用 **清理函数** 来做到这一切。
-Let's look at each of these steps in detail.
+以下是具体步骤。
-### Step 1: Declare an Effect {/*step-1-declare-an-effect*/}
+### 第一步:声明 Effect {/*step-1-declare-an-effect*/}
-To declare an Effect in your component, import the [`useEffect` Hook](/reference/react/useEffect) from React:
+首先在 React 中引入 [`useEffect` Hook](/reference/react/useEffect):
```js
import { useEffect } from 'react';
```
-Then, call it at the top level of your component and put some code inside your Effect:
+然后,在组件顶部调用它,并传入在每次渲染时都需要执行的代码:
```js {2-4}
function MyComponent() {
useEffect(() => {
- // Code here will run after *every* render
+ // 每次渲染后都会执行此处的代码
});
return
;
}
```
-Every time your component renders, React will update the screen *and then* run the code inside `useEffect`. In other words, **`useEffect` "delays" a piece of code from running until that render is reflected on the screen.**
+每当你的组件渲染时,React 将更新屏幕,然后运行 `useEffect` 中的代码。换句话说,**`useEffect` 会把这段代码放到屏幕更新渲染之后执行**。
-Let's see how you can use an Effect to synchronize with an external system. Consider a `` React component. It would be nice to control whether it's playing or paused by passing an `isPlaying` prop to it:
+让我们看看如何使用 Effect 与外部系统同步。考虑一个 `` React 组件。通过传递布尔类型的 `isPlaying` prop 以控制是播放还是暂停:
```js
;
```
-Your custom `VideoPlayer` component renders the built-in browser [``](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video) tag:
+自定义的 `VideoPlayer` 组件渲染了内置的 [``](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video) 标签:
```js
function VideoPlayer({ src, isPlaying }) {
- // TODO: do something with isPlaying
+ // TODO:使用 isPlaying 做一些事情
return ;
}
```
-However, the browser `` tag does not have an `isPlaying` prop. The only way to control it is to manually call the [`play()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/play) and [`pause()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/pause) methods on the DOM element. **You need to synchronize the value of `isPlaying` prop, which tells whether the video _should_ currently be playing, with calls like `play()` and `pause()`.**
+但是,浏览器的 `` 标签没有 `isPlaying` 属性。控制它的唯一方式是在 DOM 元素上调用 [`play()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/play) 和 [`pause()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/pause) 方法。因此,**你需要将 `isPlaying` prop 的值与 `play()` 和 `pause()` 等函数的调用进行同步,该属性用于告知当前视频是否应该播放**。
-We'll need to first [get a ref](/learn/manipulating-the-dom-with-refs) to the `` DOM node.
+首先要获取 `` DOM 节点的 [对象引用](/learn/manipulating-the-dom-with-refs)。
-You might be tempted to try to call `play()` or `pause()` during rendering, but that isn't correct:
+你可能会尝试在渲染期间调用 `play()` 或 `pause()`,但这种做法是错的:
@@ -102,9 +102,9 @@ function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
- ref.current.play(); // Calling these while rendering isn't allowed.
+ ref.current.play(); // 渲染期间不能调用 `play()`。
} else {
- ref.current.pause(); // Also, this crashes.
+ ref.current.pause(); // 同样,调用 `pause()` 也不行。
}
return ;
@@ -115,7 +115,7 @@ export default function App() {
return (
<>
setIsPlaying(!isPlaying)}>
- {isPlaying ? 'Pause' : 'Play'}
+ {isPlaying ? '暂停' : '播放'}
-The reason this code isn't correct is that it tries to do something with the DOM node during rendering. In React, [rendering should be a pure calculation](/learn/keeping-components-pure) of JSX and should not contain side effects like modifying the DOM.
+这段代码之所以不正确,是因为它试图在渲染期间对 DOM 节点进行操作。在 React 中,[JSX 的渲染必须是纯粹操作](/learn/keeping-components-pure),不应该包含任何像修改 DOM 的副作用。
-Moreover, when `VideoPlayer` is called for the first time, its DOM does not exist yet! There isn't a DOM node yet to call `play()` or `pause()` on, because React doesn't know what DOM to create until you return the JSX.
+而且,当第一次调用 `VideoPlayer` 时,对应的 DOM 节点甚至还不存在!如果连 DOM 节点都没有,那么如何调用 `play()` 或 `pause()` 方法呢!在返回 JSX 之前,React 不知道要创建什么 DOM。
-The solution here is to **wrap the side effect with `useEffect` to move it out of the rendering calculation:**
+解决办法是 **使用 `useEffect` 包裹副作用,把它分离到渲染逻辑的计算过程之外**:
```js {6,12}
import { useEffect, useRef } from 'react';
@@ -157,11 +157,11 @@ function VideoPlayer({ src, isPlaying }) {
}
```
-By wrapping the DOM update in an Effect, you let React update the screen first. Then your Effect runs.
+把调用 DOM 方法的操作封装在 Effect 中,你可以让 React 先更新屏幕,确定相关 DOM 创建好了以后然后再运行 Effect。
-When your `VideoPlayer` component renders (either the first time or if it re-renders), a few things will happen. First, React will update the screen, ensuring the `` tag is in the DOM with the right props. Then React will run your Effect. Finally, your Effect will call `play()` or `pause()` depending on the value of `isPlaying`.
+当 `VideoPlayer` 组件渲染时(无论是否为首次渲染),都会发生以下事情。首先,React 会刷新屏幕,确保 `` 元素已经正确地出现在 DOM 中;然后,React 将运行 Effect;最后,Effect 将根据 `isPlaying` 的值调用 `play()` 或 `pause()`。
-Press Play/Pause multiple times and see how the video player stays synchronized to the `isPlaying` value:
+试试按下几次播放和暂停操作,观察视频播放器的播放、暂停行为是如何与 `isPlaying` prop 同步的:
@@ -187,7 +187,7 @@ export default function App() {
return (
<>
setIsPlaying(!isPlaying)}>
- {isPlaying ? 'Pause' : 'Play'}
+ {isPlaying ? '暂停' : '播放'}
-In this example, the "external system" you synchronized to React state was the browser media API. You can use a similar approach to wrap legacy non-React code (like jQuery plugins) into declarative React components.
+在这个示例中,你同步到 React state 的“外部系统”是浏览器媒体 API;也可以使用类似的方法将旧的非 React 代码(如 jQuery 插件)封装成声明性的 React 组件。
-Note that controlling a video player is much more complex in practice. Calling `play()` may fail, the user might play or pause using the built-in browser controls, and so on. This example is very simplified and incomplete.
+请注意,控制视频播放器在实际应用中复杂得多:比如调用 `play()` 可能会失败,用户可能会使用内置浏览器控件播放或暂停等等。这只是一个简化了很多具体细节的例子。
-By default, Effects run after *every* render. This is why code like this will **produce an infinite loop:**
+一般来说,Effect 会在 **每次** 渲染后执行,**而以下代码会陷入死循环中**:
```js
const [count, setCount] = useState(0);
@@ -220,20 +220,20 @@ useEffect(() => {
});
```
-Effects run as a *result* of rendering. Setting state *triggers* rendering. Setting state immediately in an Effect is like plugging a power outlet into itself. The Effect runs, it sets the state, which causes a re-render, which causes the Effect to run, it sets the state again, this causes another re-render, and so on.
+每次渲染结束都会执行 Effect;而更新 state 会触发重新渲染。但是新一轮渲染时又会再次执行 Effect,然后 Effect 再次更新 state……如此周而复始,从而陷入死循环。
-Effects should usually synchronize your components with an *external* system. If there's no external system and you only want to adjust some state based on other state, [you might not need an Effect.](/learn/you-might-not-need-an-effect)
+Effect 通常应该使组件与 **外部** 系统保持同步。如果没有外部系统,你只想根据其他状态调整一些状态,那么 [你也许不需要 Effect](/learn/you-might-not-need-an-effect)。
-### Step 2: Specify the Effect dependencies {/*step-2-specify-the-effect-dependencies*/}
+### 第二步:指定 Effect 依赖 {/*step-2-specify-the-effect-dependencies*/}
-By default, Effects run after *every* render. Often, this is **not what you want:**
+一般来说,Effect 会在 **每次** 渲染时执行。**但更多时候,并不需要每次渲染的时候都执行 Effect**。
-- Sometimes, it's slow. Synchronizing with an external system is not always instant, so you might want to skip doing it unless it's necessary. For example, you don't want to reconnect to the chat server on every keystroke.
-- Sometimes, it's wrong. For example, you don't want to trigger a component fade-in animation on every keystroke. The animation should only play once when the component appears for the first time.
+- 有时这会拖慢运行速度。因为与外部系统的同步操作总是有一定时耗,在非必要时可能希望跳过它。例如,没有人会希望每次用键盘打字时都重新连接聊天服务器。
+- 有时这会导致程序逻辑错误。例如,组件的淡入动画只需要在第一轮渲染出现时播放一次,而不是每次触发新一轮渲染后都播放。
-To demonstrate the issue, here is the previous example with a few `console.log` calls and a text input that updates the parent component's state. Notice how typing causes the Effect to re-run:
+为了演示这个问题,我们在前面的示例中加入一些 `console.log` 语句和更新父组件 state 的文本输入。请注意键入是如何导致 Effect 重新运行的:
@@ -245,10 +245,10 @@ function VideoPlayer({ src, isPlaying }) {
useEffect(() => {
if (isPlaying) {
- console.log('Calling video.play()');
+ console.log('调用 video.play()');
ref.current.play();
} else {
- console.log('Calling video.pause()');
+ console.log('调用 video.pause()');
ref.current.pause();
}
});
@@ -263,7 +263,7 @@ export default function App() {
<>
setText(e.target.value)} />
setIsPlaying(!isPlaying)}>
- {isPlaying ? 'Pause' : 'Play'}
+ {isPlaying ? '暂停' : '播放'}
-You can tell React to **skip unnecessarily re-running the Effect** by specifying an array of *dependencies* as the second argument to the `useEffect` call. Start by adding an empty `[]` array to the above example on line 14:
+将 **依赖数组** 传入 `useEffect` 的第二个参数,以告诉 React **跳过不必要地重新运行 Effect**。在上面示例的第 14 行中传入一个空数组 `[]`:
```js {3}
useEffect(() => {
@@ -289,7 +289,7 @@ You can tell React to **skip unnecessarily re-running the Effect** by specifying
}, []);
```
-You should see an error saying `React Hook useEffect has a missing dependency: 'isPlaying'`:
+你会发现 React 报错:`React Hook useEffect has a missing dependency: 'isPlaying'`:
@@ -301,13 +301,13 @@ function VideoPlayer({ src, isPlaying }) {
useEffect(() => {
if (isPlaying) {
- console.log('Calling video.play()');
+ console.log('调用 video.play()');
ref.current.play();
} else {
- console.log('Calling video.pause()');
+ console.log('调用 video.pause()');
ref.current.pause();
}
- }, []); // This causes an error
+ }, []); // 浙江产生错误
return ;
}
@@ -337,19 +337,19 @@ video { width: 250px; }
-The problem is that the code inside of your Effect *depends on* the `isPlaying` prop to decide what to do, but this dependency was not explicitly declared. To fix this issue, add `isPlaying` to the dependency array:
+问题出现在 Effect 中使用了 `isPlaying` prop 以控制逻辑,但又没有直接告诉 Effect 需要依赖这个属性。为了解决这个问题,将 `isPlaying` 添加至依赖数组中:
```js {2,7}
useEffect(() => {
- if (isPlaying) { // It's used here...
+ if (isPlaying) { // isPlaying 在此处使用……
// ...
} else {
// ...
}
- }, [isPlaying]); // ...so it must be declared here!
+ }, [isPlaying]); // ……所以它必须在此处声明!
```
-Now all dependencies are declared, so there is no error. Specifying `[isPlaying]` as the dependency array tells React that it should skip re-running your Effect if `isPlaying` is the same as it was during the previous render. With this change, typing into the input doesn't cause the Effect to re-run, but pressing Play/Pause does:
+现在所有的依赖都已经声明,所以没有错误了。指定 `[isPlaying]` 会告诉 React,如果 `isPlaying` 在上一次渲染时与当前相同,它应该跳过重新运行 Effect。通过这个改变,输入框的输入不会导致 Effect 重新运行,但是按下播放/暂停按钮会重新运行 Effect。
@@ -397,37 +397,37 @@ video { width: 250px; }
-The dependency array can contain multiple dependencies. React will only skip re-running the Effect if *all* of the dependencies you specify have exactly the same values as they had during the previous render. React compares the dependency values using the [`Object.is`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison. See the [`useEffect` reference](/reference/react/useEffect#reference) for details.
+依赖数组可以包含多个依赖项。当指定的所有依赖项在上一次渲染期间的值与当前值完全相同时,React 会跳过重新运行该 Effect。React 使用 [`Object.is`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 比较依赖项的值。有关详细信息,请参阅 [`useEffect` 参考文档](/reference/react/useEffect#reference)。
-**Notice that you can't "choose" your dependencies.** You will get a lint error if the dependencies you specified don't match what React expects based on the code inside your Effect. This helps catch many bugs in your code. If you don't want some code to re-run, [*edit the Effect code itself* to not "need" that dependency.](/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize)
+**请注意,不能随意选择依赖项**。如果你指定的依赖项不能与 Effect 代码所期望的相匹配时,lint 将会报错,这将帮助你找到代码中的问题。如果你希望重复执行,[那么你应当 **重新编辑 Effect 代码本身**,使其不需要该依赖项](/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize)。
-The behaviors without the dependency array and with an *empty* `[]` dependency array are different:
+没有依赖数组作为第二个参数,与依赖数组位空数组 `[]` 的行为是不一致的:
```js {3,7,11}
useEffect(() => {
- // This runs after every render
+ // 这里的代码会在每次渲染后执行
});
useEffect(() => {
- // This runs only on mount (when the component appears)
+ // 这里的代码只会在组件挂载后执行
}, []);
useEffect(() => {
- // This runs on mount *and also* if either a or b have changed since the last render
+ //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);
```
-We'll take a close look at what "mount" means in the next step.
+接下来,我们将进一步介绍什么是 **挂载(mount)**。
-#### Why was the ref omitted from the dependency array? {/*why-was-the-ref-omitted-from-the-dependency-array*/}
+#### 为什么依赖数组中可以省略 ref? {/*why-was-the-ref-omitted-from-the-dependency-array*/}
-This Effect uses _both_ `ref` and `isPlaying`, but only `isPlaying` is declared as a dependency:
+下面的 Effect 同时使用了 `ref` 与 `isPlaying` prop,但是只有 `isPlaying` 被声明为了依赖项:
```js {9}
function VideoPlayer({ src, isPlaying }) {
@@ -441,7 +441,7 @@ function VideoPlayer({ src, isPlaying }) {
}, [isPlaying]);
```
-This is because the `ref` object has a *stable identity:* React guarantees [you'll always get the same object](/reference/react/useRef#returns) from the same `useRef` call on every render. It never changes, so it will never by itself cause the Effect to re-run. Therefore, it does not matter whether you include it or not. Including it is fine too:
+这是因为 `ref` 具有 **稳定** 的标识:React 保证 [每轮渲染中调用 `useRef` 所产生的引用对象时,获取到的对象引用总是相同的](/reference/react/useRef#returns),即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。因此,依赖数组中是否包含它并不重要。当然也可以包括它:,这样也可以:
```js {9}
function VideoPlayer({ src, isPlaying }) {
@@ -455,17 +455,17 @@ function VideoPlayer({ src, isPlaying }) {
}, [isPlaying, ref]);
```
-The [`set` functions](/reference/react/useState#setstate) returned by `useState` also have stable identity, so you will often see them omitted from the dependencies too. If the linter lets you omit a dependency without errors, it is safe to do.
+`useState` 返回的 [`set` 函数](/reference/react/useState#setstate) 也有稳定的标识符,所以也可以把它从依赖数组中忽略掉。如果在忽略某个依赖项时 linter 不会报错,那么这么做就是安全的。
-Omitting always-stable dependencies only works when the linter can "see" that the object is stable. For example, if `ref` was passed from a parent component, you would have to specify it in the dependency array. However, this is good because you can't know whether the parent component always passes the same ref, or passes one of several refs conditionally. So your Effect _would_ depend on which ref is passed.
+但是,仅在 linter 可以“看到”对象稳定时,忽略稳定依赖项的规则才会起作用。例如,如果 `ref` 是从父组件传递的,则必须在依赖项数组中指定它。这样做是合适的,因为无法确定父组件是否始终是传递相同的 ref,或者可能是有条件地传递几个 ref 之一。因此,你的 Effect 将取决于传递的是哪个 ref。
-### Step 3: Add cleanup if needed {/*step-3-add-cleanup-if-needed*/}
+### 第三部:按需添加清理(cleanup)函数 {/*step-3-add-cleanup-if-needed*/}
-Consider a different example. You're writing a `ChatRoom` component that needs to connect to the chat server when it appears. You are given a `createConnection()` API that returns an object with `connect()` and `disconnect()` methods. How do you keep the component connected while it is displayed to the user?
+考虑一个不同的例子。你正在编写一个 `ChatRoom` 组件,该组件出现时需要连接到聊天服务器。现在为你提供了 `createConnection()` API,该 API 返回一个包含 `connect()` 与 `disconnection()` 方法的对象。考虑当组件展示给用户时,应该如何保持连接?
-Start by writing the Effect logic:
+从编写 Effect 逻辑开始:
```js
useEffect(() => {
@@ -474,7 +474,7 @@ useEffect(() => {
});
```
-It would be slow to connect to the chat after every re-render, so you add the dependency array:
+每次重新渲染后连接到聊天室会很慢,因此可以添加依赖数组:
```js {4}
useEffect(() => {
@@ -483,9 +483,9 @@ useEffect(() => {
}, []);
```
-**The code inside the Effect does not use any props or state, so your dependency array is `[]` (empty). This tells React to only run this code when the component "mounts", i.e. appears on the screen for the first time.**
+**在这个例子中,Effect 中的代码没有使用任何 props 或 state,此时指定依赖数组为空数组 `[]`。这告诉 React 仅在组件“挂载”时运行此代码,即首次出现在屏幕上这一阶段**。
-Let's try running this code:
+试试运行下面的代码:
@@ -498,19 +498,19 @@ export default function ChatRoom() {
const connection = createConnection();
connection.connect();
}, []);
- return Welcome to the chat! ;
+ return 欢迎来到聊天室! ;
}
```
```js chat.js
export function createConnection() {
- // A real implementation would actually connect to the server
+ // 真实的实现会将其连接到服务器,此处代码只是示例
return {
connect() {
- console.log('✅ Connecting...');
+ console.log('✅ 连接中……');
},
disconnect() {
- console.log('❌ Disconnected.');
+ console.log('❌ 连接失败。');
}
};
}
@@ -522,15 +522,15 @@ input { display: block; margin-bottom: 20px; }
-This Effect only runs on mount, so you might expect `"✅ Connecting..."` to be printed once in the console. **However, if you check the console, `"✅ Connecting..."` gets printed twice. Why does it happen?**
+这里的 Effect 仅在组件挂载时执行,所以 `"✅ 连接中……"` 在控制台中只会打印一次。**然而控制台实际打印 `"✅ 连接中……"` 了两次!为什么会这样**?
-Imagine the `ChatRoom` component is a part of a larger app with many different screens. The user starts their journey on the `ChatRoom` page. The component mounts and calls `connection.connect()`. Then imagine the user navigates to another screen--for example, to the Settings page. The `ChatRoom` component unmounts. Finally, the user clicks Back and `ChatRoom` mounts again. This would set up a second connection--but the first connection was never destroyed! As the user navigates across the app, the connections would keep piling up.
+想象 `ChatRoom` 组件是一个大规模的 App 中许多界面中的一部分。用户切换到含有 `ChatRoom` 组件的页面上时,该组件被挂载,并调用 `connection.connect()` 方法连接服务器。然后想象用户此时突然导航到另一个页面,比如切换到“设置”页面。这时,`ChatRoom` 组件就被卸载了。接下来,用户在“设置”页面忙完后,单击“返回”,回到上一个页面,并再次挂载 `ChatRoom`。这将建立第二次连接,但是,第一次时创建的连接从未被销毁!当用户在应用程序中不断切换界面再返回时,与服务器的连接会不断堆积。
-Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount.
+如果不进行大量的手动测试,这样的错误很容易被遗漏。为了帮助你快速发现它们,在开发环境中,React 会在初始挂载组件后,立即再挂载一次。
-Seeing the `"✅ Connecting..."` log twice helps you notice the real issue: your code doesn't close the connection when the component unmounts.
+观察到 `"✅ 连接中……"` 出现了两次,可以帮助找到问题所在:在代码中,组件被卸载时没有关闭连接。
-To fix the issue, return a *cleanup function* from your Effect:
+为了解决这个问题,可以在 Effect 中返回一个 **清理(cleanup)** 函数。
```js {4-6}
useEffect(() => {
@@ -542,7 +542,7 @@ To fix the issue, return a *cleanup function* from your Effect:
}, []);
```
-React will call your cleanup function each time before the Effect runs again, and one final time when the component unmounts (gets removed). Let's see what happens when the cleanup function is implemented:
+每次重新执行 Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数。让我们看看执行清理函数会做些什么:
@@ -556,19 +556,19 @@ export default function ChatRoom() {
connection.connect();
return () => connection.disconnect();
}, []);
- return Welcome to the chat! ;
+ return 欢迎来到聊天室! ;
}
```
```js chat.js
export function createConnection() {
- // A real implementation would actually connect to the server
+ // 真实的实现会将其连接到服务器,此处代码只是示例
return {
connect() {
- console.log('✅ Connecting...');
+ console.log('✅ 连接中……');
},
disconnect() {
- console.log('❌ Disconnected.');
+ console.log('❌ 连接失败。');
}
};
}
@@ -580,27 +580,27 @@ input { display: block; margin-bottom: 20px; }
-Now you get three console logs in development:
+现在在开发模式下,控制台会打印三条记录:
-1. `"✅ Connecting..."`
-2. `"❌ Disconnected."`
-3. `"✅ Connecting..."`
+1. `"✅ 连接中……"`
+2. `"❌ 连接失败。"`
+3. `"✅ 连接中……"`
-**This is the correct behavior in development.** By remounting your component, React verifies that navigating away and back would not break your code. Disconnecting and then connecting again is exactly what should happen! When you implement the cleanup well, there should be no user-visible difference between running the Effect once vs running it, cleaning it up, and running it again. There's an extra connect/disconnect call pair because React is probing your code for bugs in development. This is normal--don't try to make it go away!
+**在开发环境下,出现这样的结果才是符合预期的**。重复挂载组件,可以确保在 React 中离开和返回页面时不会导致代码运行出现问题。上面的代码中规定了挂载组件时连接服务器、卸载组件时断连服务器。所以断开、连接再重新连接是符合预期的行为。当为 Effect 正确实现清理函数时,无论 Effect 执行一次,还是执行、清理、再执行,用户都不会感受到明显的差异。所以,在开发环境下,出现额外的连接、断连时,这是 React 正在调试你的代码。这是很正常的现象,不要试图消除它!
-**In production, you would only see `"✅ Connecting..."` printed once.** Remounting components only happens in development to help you find Effects that need cleanup. You can turn off [Strict Mode](/reference/react/StrictMode) to opt out of the development behavior, but we recommend keeping it on. This lets you find many bugs like the one above.
+**在生产环境下,`"✅ 连接中……"` 只会被打印一次**。也就是说仅在开发环境下才会重复挂载组件,以帮助你找到需要清理的 Effect。你可以选择关闭 [严格模式](/reference/react/StrictMode) 来关闭开发环境下特有的行为,但我们建议保留它。这可以帮助发现许多上面这样的错误。
-## How to handle the Effect firing twice in development? {/*how-to-handle-the-effect-firing-twice-in-development*/}
+## 如何处理在开发环境中 Effect 执行两次? {/*how-to-handle-the-effect-firing-twice-in-development*/}
-React intentionally remounts your components in development to find bugs like in the last example. **The right question isn't "how to run an Effect once", but "how to fix my Effect so that it works after remounting".**
+在开发环境中,React 有意重复挂载你的组件,以查找像上面示例中的错误。**正确的态度是“如何修复 Effect 以便它在重复挂在后能正常工作”,而不是“如何只运行一次 Effect”**。
-Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn't be able to distinguish between the Effect running once (as in production) and a _setup → cleanup → setup_ sequence (as you'd see in development).
+通常的解决办法是实现清理函数。清理函数应该停止或撤销 Effect 正在执行的任何操作。简单来说,用户不应该感受到 Effect 只执行一次(如在生产环境中)和执行“挂载 → 清理 → 挂载”过程(如在开发环境中)之间的差异。
-Most of the Effects you'll write will fit into one of the common patterns below.
+下面提供一些常用的 Effect 应用模式。
-### Controlling non-React widgets {/*controlling-non-react-widgets*/}
+### 控制非 React 组件 {/*controlling-non-react-widgets*/}
-Sometimes you need to add UI widgets that aren't written to React. For example, let's say you're adding a map component to your page. It has a `setZoomLevel()` method, and you'd like to keep the zoom level in sync with a `zoomLevel` state variable in your React code. Your Effect would look similar to this:
+有时需要添加不是使用 React 编写的 UI 小部件。例如,假设你要向页面添加地图组件,并且它有一个 `setZoomLevel()` 方法,你希望调整缩放级别(zoom level)并与 React 代码中的 `zoomLevel` state 变量保持同步。Effect 看起来应该与下面类似:
```js
useEffect(() => {
@@ -609,9 +609,9 @@ useEffect(() => {
}, [zoomLevel]);
```
-Note that there is no cleanup needed in this case. In development, React will call the Effect twice, but this is not a problem because calling `setZoomLevel` twice with the same value does not do anything. It may be slightly slower, but this doesn't matter because it won't remount needlessly in production.
+请注意,在这种情况下不需要清理。在开发环境中,React 会调用 Effect 两次,但这两次挂载时依赖项 `setZoomLevel` 都是相同的,所以会跳过执行第二次挂载时的 Effect。开发环境中它可能会稍微慢一些,但这问题不大,因为它在生产中不会进行不必要的重复挂载。
-Some APIs may not allow you to call them twice in a row. For example, the [`showModal`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLDialogElement/showModal) method of the built-in [``](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLDialogElement) element throws if you call it twice. Implement the cleanup function and make it close the dialog:
+某些 API 可能不允许连续调用两次。例如,内置的 [``](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLDialogElement) 元素的 [`showModal`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLDialogElement/showModal) 方法在连续调用两次时会抛出异常,此时实现清理函数并使其关闭对话框:
```js {4}
useEffect(() => {
@@ -621,11 +621,11 @@ useEffect(() => {
}, []);
```
-In development, your Effect will call `showModal()`, then immediately `close()`, and then `showModal()` again. This has the same user-visible behavior as calling `showModal()` once, as you would see in production.
+在开发环境中,Effect 将调用 `showModal()`,然后立即调用 `close()`,然后再次调用 `showModal()`。这与调用只一次 `showModal()` 的效果相同。也正如在生产环境中看到的那样。
-### Subscribing to events {/*subscribing-to-events*/}
+### 订阅事件 {/*subscribing-to-events*/}
-If your Effect subscribes to something, the cleanup function should unsubscribe:
+如果 Effect 订阅了某些事件,清理函数应该退订这些事件:
```js {6}
useEffect(() => {
@@ -637,27 +637,27 @@ useEffect(() => {
}, []);
```
-In development, your Effect will call `addEventListener()`, then immediately `removeEventListener()`, and then `addEventListener()` again with the same handler. So there would be only one active subscription at a time. This has the same user-visible behavior as calling `addEventListener()` once, as in production.
+在开发环境中,Effect 会调用两次 `addEventListener()`,然后立即调用 `removeEventListener()`,然后再调用相同的 `addEventListener()`,这与只订阅一次事件的 Effect 等效;这也与用户在生产环境中只调用一次 `addEventListener()` 具有相同的感知效果。
-### Triggering animations {/*triggering-animations*/}
+### 触发动画 {/*triggering-animations*/}
-If your Effect animates something in, the cleanup function should reset the animation to the initial values:
+如果 Effect 对某些内容加入了动画,清理函数应将动画重置:
```js {4-6}
useEffect(() => {
const node = ref.current;
- node.style.opacity = 1; // Trigger the animation
+ node.style.opacity = 1; // 触发动画
return () => {
- node.style.opacity = 0; // Reset to the initial value
+ node.style.opacity = 0; // 重置为初始值
};
}, []);
```
-In development, opacity will be set to `1`, then to `0`, and then to `1` again. This should have the same user-visible behavior as setting it to `1` directly, which is what would happen in production. If you use a third-party animation library with support for tweening, your cleanup function should reset the timeline to its initial state.
+在开发环境中,透明度由 `1` 变为 `0`,再变为 `1`。这与在生产环境中,直接将其设置为 `1` 具有相同的感知效果,如果你使用支持过渡的第三方动画库,你的清理函数应将时间轴重置为其初始状态。
-### Fetching data {/*fetching-data*/}
+### 获取数据 {/*fetching-data*/}
-If your Effect fetches something, the cleanup function should either [abort the fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController) or ignore its result:
+如果 Effect 将会获取数据,清理函数应该要么 [中止该数据获取操作](https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController),要么忽略其结果:
```js {2,6,13-15}
useEffect(() => {
@@ -678,11 +678,11 @@ useEffect(() => {
}, [userId]);
```
-You can't "undo" a network request that already happened, but your cleanup function should ensure that the fetch that's _not relevant anymore_ does not keep affecting your application. If the `userId` changes from `'Alice'` to `'Bob'`, cleanup ensures that the `'Alice'` response is ignored even if it arrives after `'Bob'`.
+我们无法撤消已经发生的网络请求,但是清理函数应当确保获取数据的过程以及获取到的结果不会继续影响程序运行。如果 `userId` 从 `'Alice'` 变为 `'Bob'`,那么请确保 `'Alice'` 响应数据被忽略,即使它在 `'Bob'` 之后到达。
-**In development, you will see two fetches in the Network tab.** There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned up so its copy of the `ignore` variable will be set to `true`. So even though there is an extra request, it won't affect the state thanks to the `if (!ignore)` check.
+**在开发环境中,浏览器调试工具的“网络”选项卡中会出现两个 fetch 请求**。这是正常的。使用上述方法,第一个 Effect 将立即被清理,而 `ignore` 将被设置为 `true`。因此,即使有额外的请求,由于有 `if (!ignore)` 判断检查,也不会影响程序状态。
-**In production, there will only be one request.** If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
+**在生产环境中,只会显示发送了一条获取请求**。如果开发环境中,第二次请求给你造成了困扰,最好的方法是使用一种可以删除重复请求、并缓存请求响应的解决方案:
```js
function TodoList() {
@@ -690,90 +690,90 @@ function TodoList() {
// ...
```
-This will not only improve the development experience, but also make your application feel faster. For example, the user pressing the Back button won't have to wait for some data to load again because it will be cached. You can either build such a cache yourself or use one of the many alternatives to manual fetching in Effects.
+这不仅可以提高开发体验,还可以让你的应用程序速度更快。例如,用户按下按钮时,如果数据已经被缓存了,那么就不必再次等待加载。你可以自己构建这样的缓存,也可以使用很多在 Effect 中手动加载数据的替代方法。
-#### What are good alternatives to data fetching in Effects? {/*what-are-good-alternatives-to-data-fetching-in-effects*/}
+#### Effect 中有哪些好的数据获取替代方案? {/*what-are-good-alternatives-to-data-fetching-in-effects*/}
-Writing `fetch` calls inside Effects is a [popular way to fetch data](https://www.robinwieruch.de/react-hooks-fetch-data/), especially in fully client-side apps. This is, however, a very manual approach and it has significant downsides:
+在 Effect 中调用 `fetch` 请求数据 [是一种非常受欢迎的方式](https://www.robinwieruch.de/react-hooks-fetch-data/),特别是在客户端应用中。然而,它非常依赖手动操作,有很多的缺点:
-- **Effects don't run on the server.** This means that the initial server-rendered HTML will only include a loading state with no data. The client computer will have to download all JavaScript and render your app only to discover that now it needs to load the data. This is not very efficient.
-- **Fetching directly in Effects makes it easy to create "network waterfalls".** You render the parent component, it fetches some data, renders the child components, and then they start fetching their data. If the network is not very fast, this is significantly slower than fetching all data in parallel.
-- **Fetching directly in Effects usually means you don't preload or cache data.** For example, if the component unmounts and then mounts again, it would have to fetch the data again.
-- **It's not very ergonomic.** There's quite a bit of boilerplate code involved when writing `fetch` calls in a way that doesn't suffer from bugs like [race conditions.](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect)
+- **Effect 不能在服务端执行**。这意味着服务器最初传递的 HTML 不会包含任何数据。客户端的浏览器必须下载所有 JavaScript 脚本来渲染应用程序,然后才能加载数据——这并不搞笑。
+- **直接在 Effect 中获取数据容易产生网络瀑布(network waterfall)**。首先渲染了父组件,它会获取一些数据并进行渲染;然后渲染子组件,接着子组件开始获取它们的数据。如果网络速度不够快,这种方式比同时获取所有数据要慢得多。
+- **直接在 Effect 中获取数据通常意味着无法预加载或缓存数据**。例如,在组件卸载后然后再次挂载,那么它必须再次获取数据。
+- **这不是很符合人机交互原则**。如果你不想出现像 [条件竞争(race condition)](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect) 之类的问题,那么你需要编写更多的样板代码。
-This list of downsides is not specific to React. It applies to fetching data on mount with any library. Like with routing, data fetching is not trivial to do well, so we recommend the following approaches:
+以上所列出来的缺点并不是 React 特有的。在任何框架或者库上的组件挂载过程中获取数据,都会遇到这些问题。与路由一样,要做好数据获取并非易事,因此我们推荐以下方法:
-- **If you use a [framework](/learn/start-a-new-react-project#production-grade-react-frameworks), use its built-in data fetching mechanism.** Modern React frameworks have integrated data fetching mechanisms that are efficient and don't suffer from the above pitfalls.
-- **Otherwise, consider using or building a client-side cache.** Popular open source solutions include [React Query](https://tanstack.com/query/latest), [useSWR](https://swr.vercel.app/), and [React Router 6.4+.](https://beta.reactrouter.com/en/main/start/overview) You can build your own solution too, in which case you would use Effects under the hood, but add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).
+- **如果你正在使用 [框架](/learn/start-a-new-react-project#production-grade-react-frameworks) ,使用其内置的数据获取机制**。现代 React 框架集成了高效的数据获取机制,不会出现上述问题。
+- **否则,请考虑使用或构建客户端缓存**。目前受欢迎的开源解决方案是 [React Query](https://tanstack.com/query/latest)、[useSWR](https://swr.vercel.app/) 和 [React Router v6.4+](https://beta.reactrouter.com/en/main/start/overview)。你也可以构建自己的解决方案,在这种情况下,你可以在幕后使用 Effect,但是请注意添加用于删除重复请求、缓存响应和避免网络瀑布(通过预加载数据或将数据需求提升到路由)的逻辑。
-You can continue fetching data directly in Effects if neither of these approaches suit you.
+如果这些方法都不适合你,你可以继续直接在 Effect 中获取数据。
-### Sending analytics {/*sending-analytics*/}
+### 发送分析报告 {/*sending-analytics*/}
-Consider this code that sends an analytics event on the page visit:
+考虑在访问页面时发送日志分析:
```js
useEffect(() => {
- logVisit(url); // Sends a POST request
+ logVisit(url); // 发送 POST 请求
}, [url]);
```
-In development, `logVisit` will be called twice for every URL, so you might be tempted to try to fix that. **We recommend keeping this code as is.** Like with earlier examples, there is no *user-visible* behavior difference between running it once and running it twice. From a practical point of view, `logVisit` should not do anything in development because you don't want the logs from the development machines to skew the production metrics. Your component remounts every time you save its file, so it logs extra visits in development anyway.
+在开发环境中,`logVisit` 会为每个 URL 发送两次请求,所以你可能会想尝试解决这个问题。**不过我们建议不必修改此处代码**,与前面的示例一样,从用户的角度来看,运行一次和运行两次之间不会 **感知** 到行为差异。从实际的角度来看,`logVisit` 不应该在开发环境中做任何影响生产事情。由于每次保存代码文件时都会重新挂载组件,因此在开发环境中会额外记录访问次数。
-**In production, there will be no duplicate visit logs.**
+**在生产环境中,不会产生有重复的访问日志**。
-To debug the analytics events you're sending, you can deploy your app to a staging environment (which runs in production mode) or temporarily opt out of [Strict Mode](/reference/react/StrictMode) and its development-only remounting checks. You may also send analytics from the route change event handlers instead of Effects. For more precise analytics, [intersection observers](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API) can help track which components are in the viewport and how long they remain visible.
+为了调试发送的分析事件,可以将应用部署到一个运行在生产模式下的暂存环境,或者暂时取消 [严格模式](/reference/react/StrictMode) 及其仅在开发环境中重新加载检查;还可以从路由变更事件处理程序中发送分析数据,而不是从 Effect 中发送。为了更精确的分析,可以使用 [Intersection Observer](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API) 来跟踪哪些组件位于视口中以及它们保持可见的时间。
-### Not an Effect: Initializing the application {/*not-an-effect-initializing-the-application*/}
+### 初始化应用时不需要使用 Effect 的情形 {/*not-an-effect-initializing-the-application*/}
-Some logic should only run once when the application starts. You can put it outside your components:
+某些逻辑应该只在应用程序启动时运行一次。比如,验证登陆状态和加载本地程序数据。你可以将其放在组件之外:
```js {2-3}
-if (typeof window !== 'undefined') { // Check if we're running in the browser.
+if (typeof window !== 'undefined') { // 检查是否在浏览器中运行
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
- // ...
+ // ……
}
```
-This guarantees that such logic only runs once after the browser loads the page.
+这保证了这种逻辑在浏览器加载页面后只运行一次。
-### Not an Effect: Buying a product {/*not-an-effect-buying-a-product*/}
+### 不要在 Effect 中执行购买商品一类的操作 {/*not-an-effect-buying-a-product*/}
-Sometimes, even if you write a cleanup function, there's no way to prevent user-visible consequences of running the Effect twice. For example, maybe your Effect sends a POST request like buying a product:
+有时,即使编写了一个清理函数,也不能避免执行两次 Effect。例如,Effect 包含会发送 POST 请求以执行购买操作:
```js {2-3}
useEffect(() => {
- // 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
+ // 🔴 错误:此处的 Effect 会在开发环境中执行两次,这在代码中是有问题的。
fetch('/api/buy', { method: 'POST' });
}, []);
```
-You wouldn't want to buy the product twice. However, this is also why you shouldn't put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You don't want to buy the product when the user *visits* a page; you want to buy it when the user *clicks* the Buy button.
+一方面,开发环境下,Effect 会执行两次,这意味着购买操作执行了两次,但是这并非是预期的结果,所以不应该把这个业务逻辑放在 Effect 中。另一方面,如果用户转到另一个页面,然后按“后退”按钮回到了这个界面,该怎么办?Effect 会随着组件再次挂载而再次执行。所以,当用户重新访问某个页面时,不应当执行购买操作;当只有用户点击“购买”按钮时,才执行购买操作。
-Buying is not caused by rendering; it's caused by a specific interaction. It should run only when the user presses the button. **Delete the Effect and move your `/api/buy` request into the Buy button event handler:**
+因此,“购买”的操作不应由组件的挂载、渲染引起的;它是由特定的交互作用引起的,它应该只在用户按下按钮时运行。因此,**它不应该写在 Effect 中,应当把 `/api/buy` 请求操作移动到购买按钮事件处理程序中**:
```js {2-3}
function handleClick() {
- // ✅ Buying is an event because it is caused by a particular interaction.
+ // ✅ 购买商品应当在事件中执行,因为这是由特定的操作引起的。
fetch('/api/buy', { method: 'POST' });
}
```
-**This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs.** From the user's perspective, visiting a page shouldn't be different from visiting it, clicking a link, and pressing Back. React verifies that your components abide by this principle by remounting them once in development.
+**这个例子说明如果重新挂载破坏了程序的逻辑,则通常含有未被发现的错误**。从用户的角度来看,访问这个页面的效果,与访问该页面时单击和页面中其他链接并按下后退没有什么不同。React 通过在开发环境中重复挂载组件来验证组件是否遵守此原则。
-## Putting it all together {/*putting-it-all-together*/}
+## 总结 {/*putting-it-all-together*/}
-This playground can help you "get a feel" for how Effects work in practice.
+下面的 playground 可以帮助你在实践中找到对 Effect 的感觉。
-This example uses [`setTimeout`](https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout) to schedule a console log with the input text to appear three seconds after the Effect runs. The cleanup function cancels the pending timeout. Start by pressing "Mount the component":
+这个例子使用 [`setTimeout`](https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout) 来安排控制台日志,在 Effect 运行后三秒钟显示输入文本。清理函数会取消挂起的超时。从按下“挂载组件”开始:
@@ -788,11 +788,11 @@ function Playground() {
console.log('⏰ ' + text);
}
- console.log('🔵 Schedule "' + text + '" log');
+ console.log('🔵 安排 "' + text + '" 日志');
const timeoutId = setTimeout(onTimeout, 3000);
return () => {
- console.log('🟡 Cancel "' + text + '" log');
+ console.log('🟡 取消 "' + text + '" 日志');
clearTimeout(timeoutId);
};
}, [text]);
@@ -800,7 +800,7 @@ function Playground() {
return (
<>
- What to log:{' '}
+ 日志内容:{' '}
setText(e.target.value)}
@@ -816,7 +816,7 @@ export default function App() {
return (
<>
setShow(!show)}>
- {show ? 'Unmount' : 'Mount'} the component
+ {show ? '卸载' : '挂载'} 组件
{show && }
{show && }
@@ -827,21 +827,21 @@ export default function App() {
-You will see three logs at first: `Schedule "a" log`, `Cancel "a" log`, and `Schedule "a" log` again. Three second later there will also be a log saying `a`. As you learned earlier, the extra schedule/cancel pair is because React remounts the component once in development to verify that you've implemented cleanup well.
+在最开始时可以看到三个日志输出:`安排 "a" 日志`,`取消 "a" 日志`,还有一个 `安排 "a" 日志`。三秒后,还会有一条日志显示:`a`。正如之前所说,额外的安排/取消动作产生的原因是因为 React 在开发环境中,会重新挂载组件一次,以验证你是否正确地实现了清理函数。
-Now edit the input to say `abc`. If you do it fast enough, you'll see `Schedule "ab" log` immediately followed by `Cancel "ab" log` and `Schedule "abc" log`. **React always cleans up the previous render's Effect before the next render's Effect.** This is why even if you type into the input fast, there is at most one timeout scheduled at a time. Edit the input a few times and watch the console to get a feel for how Effects get cleaned up.
+现在编辑输入框,输入 `abc`。如果输入速度足够快,你会看到 `安排 "ab" 日志`,紧接着的是 `取消 "ab" 日志` 和 `安排 "abc" 日志`。**React 总是在执行下一轮渲染的 Effect 之前清理上一轮渲染的 Effect**。这就是为什么即使你快速输入,最多也只有一个安排操作。试试多次编辑输入框,并观察控制台以了解 Effect 是如何被清理的。
-Type something into the input and then immediately press "Unmount the component". Notice how unmounting cleans up the last render's Effect. Here, it clears the last timeout before it has a chance to fire.
+在输入框中输入一些内容,然后立即按下“卸载组件”按钮。注意卸载时如何清理最后一轮渲染的 Effect。在这里,它在触发卸载之前,清除了最后一次安排操作。
-Finally, edit the component above and comment out the cleanup function so that the timeouts don't get cancelled. Try typing `abcde` fast. What do you expect to happen in three seconds? Will `console.log(text)` inside the timeout print the *latest* `text` and produce five `abcde` logs? Give it a try to check your intuition!
+最后,在上面的代码中注释掉清理函数,这样安排操作就不会被取消。尝试快速输入 `abcde`。你预期三秒钟内会发生什么?计时器安排内的 `console.log(text)` 会打印 **最新** 的 `text` 并产生五个 `abcde` 日志吗?验证你的直觉吧!
-Three seconds later, you should see a sequence of logs (`a`, `ab`, `abc`, `abcd`, and `abcde`) rather than five `abcde` logs. **Each Effect "captures" the `text` value from its corresponding render.** It doesn't matter that the `text` state changed: an Effect from the render with `text = 'ab'` will always see `'ab'`. In other words, Effects from each render are isolated from each other. If you're curious how this works, you can read about [closures](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures).
+三秒后,你应该看到一系列日志:`a`、`ab`、`abc`、`abcd` 与 `abcde`,而不是五个 `abcde`。**每个 Effect 都会“捕获”其对应渲染的 `text` 值**。`text` state 发生变化并不重要:来自 `text = 'ab'` 的渲染的 Effect 始终会得到 `'ab'`。换句话说,每个渲染的 Effect 都是相互隔离的。如果你对这是如何工作的感到好奇,你可以阅读有关 [闭包](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures) 的内容。
-#### Each render has its own Effects {/*each-render-has-its-own-effects*/}
+#### 每一轮渲染都有自己的 Effect {/*each-render-has-its-own-effects*/}
-You can think of `useEffect` as "attaching" a piece of behavior to the render output. Consider this Effect:
+你可以将 `useEffect` 认为其将一段行为“附加”到渲染输出。考虑这种情况:
```js
export default function ChatRoom({ roomId }) {
@@ -851,123 +851,123 @@ export default function ChatRoom({ roomId }) {
return () => connection.disconnect();
}, [roomId]);
- return Welcome to {roomId}! ;
+ return 欢迎来到 {roomId}! ;
}
```
-Let's see what exactly happens as the user navigates around the app.
+让我们看看当用户在应用程序中切换页面时到底发生了什么。
-#### Initial render {/*initial-render*/}
+#### 初始渲染 {/*initial-render*/}
-The user visits ` `. Let's [mentally substitute](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time) `roomId` with `'general'`:
+用户访问 ` `,在这里让我们 [假设](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time) `roomId` 的值为 `'general'` :
```js
- // JSX for the first render (roomId = "general")
- return Welcome to general! ;
+ // 首次渲染时的 JSX(roomId 为 "general")
+ return 欢迎来到 general! ;
```
-**The Effect is *also* a part of the rendering output.** The first render's Effect becomes:
+**Effect 也是渲染输出的一部分**。首次渲染的 Effect 变为:
```js
- // Effect for the first render (roomId = "general")
+ //首先渲染时的 Effect(roomId 为 "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
- // Dependencies for the first render (roomId = "general")
+ // 首次渲染时的依赖项(roomId 为 "general")
['general']
```
-React runs this Effect, which connects to the `'general'` chat room.
+React 将会执行用于连接到 `'general'` 聊天室的 Effect。
-#### Re-render with same dependencies {/*re-render-with-same-dependencies*/}
+#### 依赖项相同时的重新渲染 {/*re-render-with-same-dependencies*/}
-Let's say ` ` re-renders. The JSX output is the same:
+让我们探讨下 ` ` 的重复渲染。JSX 的输出结果仍然相同:
```js
- // JSX for the second render (roomId = "general")
+ // 第二次渲染时的 JSX(roomId 为 "general")
return Welcome to general! ;
```
-React sees that the rendering output has not changed, so it doesn't update the DOM.
+React 看到渲染输出没有改变,所以它不会更新 DOM 。
-The Effect from the second render looks like this:
+第二次渲染的 Effect 如下所示:
```js
- // Effect for the second render (roomId = "general")
+ // 第二次渲染时的 Effect(roomId 为 "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
- // Dependencies for the second render (roomId = "general")
+ // 第二次渲染时的依赖项(roomId 为 "general")
['general']
```
-React compares `['general']` from the second render with `['general']` from the first render. **Because all dependencies are the same, React *ignores* the Effect from the second render.** It never gets called.
+React 将第二次渲染时的 `['general']` 与第一次渲染时的 `['general']` 进行比较。**因为所有的依赖项都是相同的,React 会忽略第二次渲染时的 Effect**。所以此时 Effect 不会被调用。
-#### Re-render with different dependencies {/*re-render-with-different-dependencies*/}
+#### 依赖项不同时的重新渲染 {/*re-render-with-different-dependencies*/}
-Then, the user visits ` `. This time, the component returns different JSX:
+接下来,用户开始访问 ` `。注意这里 `roomId` 的属性值改为了 `'travel'`,返回的是不同的 JSX 输出结果:
```js
- // JSX for the third render (roomId = "travel")
- return Welcome to travel! ;
+ // 第三次渲染时的 JSX(roomId 为 "travel")
+ return 欢迎来到 travel! ;
```
-React updates the DOM to change `"Welcome to general"` into `"Welcome to travel"`.
+这时的 React 会更新 DOM ,将 `"欢迎来到 general"` 更新为 `"欢迎来到 travel"`。
-The Effect from the third render looks like this:
+第三次渲染的 Effect 如下所示:
```js
- // Effect for the third render (roomId = "travel")
+ // 第三次渲染时的 Effect(roomId 为 "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
- // Dependencies for the third render (roomId = "travel")
+ // 第三次渲染时的依赖项(roomId 为 "travel")
['travel']
```
-React compares `['travel']` from the third render with `['general']` from the second render. One dependency is different: `Object.is('travel', 'general')` is `false`. The Effect can't be skipped.
+React 将第三次渲染时的 `['travel']` 与第二次渲染时的 `['general']` 相互比较。会发现依赖项不同:`Object.is('travel', 'general')` 为 `false`:所以这次的 Effect 不能跳过。
-**Before React can apply the Effect from the third render, it needs to clean up the last Effect that _did_ run.** The second render's Effect was skipped, so React needs to clean up the first render's Effect. If you scroll up to the first render, you'll see that its cleanup calls `disconnect()` on the connection that was created with `createConnection('general')`. This disconnects the app from the `'general'` chat room.
+**在 React 执行第三次渲染的 Effect 之前,它需要清理最近渲染的 Effect**。第二次渲染的 Effect 被跳过了。所以 React 需要清理第一次渲染时的 Effect。如果你回看第一次渲染的 Effect,你可以看到第一次渲染时的清理函数需要执行的内容,是在 `createConnection('general')` 所创建的连接上调用 `disconnect()`。也就是从 `'general'` 聊天室断开连接。
-After that, React runs the third render's Effect. It connects to the `'travel'` chat room.
+之后,React 执行第三次渲染的 Effect。它连接到 `'travel'` 聊天室。
-#### Unmount {/*unmount*/}
+#### 组件卸载 {/*unmount*/}
-Finally, let's say the user navigates away, and the `ChatRoom` component unmounts. React runs the last Effect's cleanup function. The last Effect was from the third render. The third render's cleanup destroys the `createConnection('travel')` connection. So the app disconnects from the `'travel'` room.
+最后,假设用户离开了当前页面,`ChatRoom` 组件将被卸载时,React 会执行最近的 Effect 的清理函数,也就是第三次渲染时 Effect 的清理函数。第三次渲染后再清理时,清理函数破坏了 `createConnection('travel')` 方法创建的连接。因此,该应用程序与 `travel` 房间断开了连接。
-#### Development-only behaviors {/*development-only-behaviors*/}
+#### 仅开发环境下的行为 {/*development-only-behaviors*/}
-When [Strict Mode](/reference/react/StrictMode) is on, React remounts every component once after mount (state and DOM are preserved). This [helps you find Effects that need cleanup](#step-3-add-cleanup-if-needed) and exposes bugs like race conditions early. Additionally, React will remount the Effects whenever you save a file in development. Both of these behaviors are development-only.
+在 [严格模式](/reference/react/StrictMode) 下,React 在每次卸载组件后都会重新挂载组件(但是组件的 state 与 创建的 DOM 都会被保留)。[它可以帮助你找出需要添加清理函数的 Effect](#step-3-add-cleanup-if-needed),以及早暴露出像条件竞争那样的问题。此外,每当你在开发环境中保存更新代码文件时,React 也会重新挂载 Effect,不过这两种行为都仅限于开发环境。
-- Unlike events, Effects are caused by rendering itself rather than a particular interaction.
-- Effects let you synchronize a component with some external system (third-party API, network, etc).
-- By default, Effects run after every render (including the initial one).
-- React will skip the Effect if all of its dependencies have the same values as during the last render.
-- You can't "choose" your dependencies. They are determined by the code inside the Effect.
-- Empty dependency array (`[]`) corresponds to the component "mounting", i.e. being added to the screen.
-- In Strict Mode, React mounts components twice (in development only!) to stress-test your Effects.
-- If your Effect breaks because of remounting, you need to implement a cleanup function.
-- React will call your cleanup function before the Effect runs next time, and during the unmount.
+- 与事件不同,Effect 是由渲染本身,而非特定交互引起的。
+- Effect 允许你将组件与某些外部系统(第三方 API、网络等)同步。
+- 默认情况下,Effect 在每次渲染(包括初始渲染)后运行。
+- 如果 React 的所有依赖项都与上次渲染时的值相同,则将跳过本次 Effect。
+- 不能随意选择依赖项,它们是由 Effect 内部的代码决定的。
+- 空的依赖数组(`[]`)对应于组件“挂载”,即添加到屏幕上。
+- 仅在严格模式下的开发环境中,React 会挂载两次组件,以对 Effect 进行压力测试。
+- 如果 Effect 因为重新挂载而中断,那么需要实现一个清理函数。
+- React 将在下次 Effect 运行之前以及卸载期间这两个时候调用清理函数。
-#### Focus a field on mount {/*focus-a-field-on-mount*/}
+#### 挂载后聚焦于表单字段 {/*focus-a-field-on-mount*/}
-In this example, the form renders a ` ` component.
+在下面的例子中,表单中渲染了一个 ` ` 组件。
-Use the input's [`focus()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/focus) method to make `MyInput` automatically focus when it appears on the screen. There is already a commented out implementation, but it doesn't quite work. Figure out why it doesn't work, and fix it. (If you're familiar with the `autoFocus` attribute, pretend that it does not exist: we are reimplementing the same functionality from scratch.)
+使用输入框的 [`focus()`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/focus) 方法,以在 `MyInput` 出现在屏幕上时自动聚焦。下面有一种实现方式,但是被注释掉了,因为它并没有很好地工作。找出它为什么不起作用,并修复它。如果你熟悉 `autoFocus` 属性,请假装它不存在:我们正在从头开始重新实现相同的功能。
@@ -977,7 +977,7 @@ import { useEffect, useRef } from 'react';
export default function MyInput({ value, onChange }) {
const ref = useRef(null);
- // TODO: This doesn't quite work. Fix it.
+ // TODO:下面的这种做法不会生效,请修复。
// ref.current.focus()
return (
@@ -1000,13 +1000,13 @@ export default function Form() {
const [upper, setUpper] = useState(false);
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} form
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}表单
{show && (
<>
- Enter your name:
+ 输入你的姓名:
setName(e.target.value)}
@@ -1018,9 +1018,9 @@ export default function Form() {
checked={upper}
onChange={e => setUpper(e.target.checked)}
/>
- Make it uppercase
+ 大写
- Hello, {upper ? name.toUpperCase() : name}
+ 你好, {upper ? name.toUpperCase() : name}
>
)}
>
@@ -1043,15 +1043,15 @@ body {
-To verify that your solution works, press "Show form" and verify that the input receives focus (becomes highlighted and the cursor is placed inside). Press "Hide form" and "Show form" again. Verify the input is highlighted again.
+按下“展示表单”按钮并验证是否聚焦于 ` `(高亮显示,光标位于内部);再次按下“隐藏表单”和“展示表单”,以验证是否再次聚焦于输入框。
-`MyInput` should only focus _on mount_ rather than after every render. To verify that the behavior is right, press "Show form" and then repeatedly press the "Make it uppercase" checkbox. Clicking the checkbox should _not_ focus the input above it.
+仅在 **挂载** 时聚焦于 `MyInput`,而非每次渲染后。为了验证这一行为,按下“展示表单”,然后重复按下“大写”的复选框。点击复选框时,上方的输入框不应该获取焦点。
-Calling `ref.current.focus()` during render is wrong because it is a *side effect*. Side effects should either be placed inside an event handler or be declared with `useEffect`. In this case, the side effect is _caused_ by the component appearing rather than by any specific interaction, so it makes sense to put it in an Effect.
+在渲染期间调用 `ref.current.focus()` 本身是不正确的。因为这会产生“副作用”。副作用要么应该放在事件处理程序里面,要么在 `useEffect` 中。在这种情况下,副作用是组件渲染引起的,而不是任何特定的交互引起的,因此应该将它放在 Effect 中。
-To fix the mistake, wrap the `ref.current.focus()` call into an Effect declaration. Then, to ensure that this Effect runs only on mount rather than after every render, add the empty `[]` dependencies to it.
+为了修复这个错误,可以对 `ref.current.focus()` 的调用包裹在 Effect 中。然后确保这个 Effect 只在组件挂载时执行而不是在每一轮渲染时都执行,可以为 Effect 的声明加一个空的依赖数组 `[]`。
@@ -1085,13 +1085,13 @@ export default function Form() {
const [upper, setUpper] = useState(false);
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} form
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}表单
{show && (
<>
- Enter your name:
+ 输入你的姓名:
setName(e.target.value)}
@@ -1103,9 +1103,9 @@ export default function Form() {
checked={upper}
onChange={e => setUpper(e.target.checked)}
/>
- Make it uppercase
+ 大写
- Hello, {upper ? name.toUpperCase() : name}
+ 你好,{upper ? name.toUpperCase() : name}
>
)}
>
@@ -1129,13 +1129,13 @@ body {
-#### Focus a field conditionally {/*focus-a-field-conditionally*/}
+#### 有条件地聚焦于表单 {/*focus-a-field-conditionally*/}
-This form renders two ` ` components.
+下面的表单渲染两个 ` ` 组件。
-Press "Show form" and notice that the second field automatically gets focused. This is because both of the ` ` components try to focus the field inside. When you call `focus()` for two input fields in a row, the last one always "wins".
+按下“展示表单”,同时注意自动聚焦于第二个输入框,这是因为两个 ` ` 组件都在试图把焦点往自身上转移。当你连续为两个输入框调用 `focus()` 时,总是聚焦于最后面的输入框。
-Let's say you want to focus the first field. The first `MyInput` component now receives a boolean `shouldFocus` prop set to `true`. Change the logic so that `focus()` is only called if the `shouldFocus` prop received by `MyInput` is `true`.
+假设聚焦于第一个输入框,那么,第一个 `MyInput` 组件现在接收到 `shouldFocus` 属性,并且应当被设置为 `true`。更改下程序逻辑,规定仅当 `MyInput` 接收到的 `shouldFocus` 属性为 `true` 时才调用 `focus()` 。
@@ -1145,7 +1145,7 @@ import { useEffect, useRef } from 'react';
export default function MyInput({ shouldFocus, value, onChange }) {
const ref = useRef(null);
- // TODO: call focus() only if shouldFocus is true.
+ // TODO:只在 shouldFocus 为 true 时才调用 focus()
useEffect(() => {
ref.current.focus();
}, []);
@@ -1172,13 +1172,13 @@ export default function Form() {
const name = firstName + ' ' + lastName;
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} form
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}表单
{show && (
<>
- Enter your first name:
+ 输入你的名:
setFirstName(e.target.value)}
@@ -1186,14 +1186,14 @@ export default function Form() {
/>
- Enter your last name:
+ 输入你的姓:
setLastName(e.target.value)}
shouldFocus={false}
/>
- Hello, {upper ? name.toUpperCase() : name}
+ 你好,{upper ? name.toUpperCase() : name}
>
)}
>
@@ -1215,17 +1215,17 @@ body {
-To verify your solution, press "Show form" and "Hide form" repeatedly. When the form appears, only the *first* input should get focused. This is because the parent component renders the first input with `shouldFocus={true}` and the second input with `shouldFocus={false}`. Also check that both inputs still work and you can type into both of them.
+试着重复按下“展示表单”和“隐藏表单”以验证你的解决方式。当表单出现时,这里只有第一个输入框获得了焦点。那是因为它的父组件渲染的第一个输入框时,第一个输入框带着 `shouldFocus={true}` 这个属性值,而渲染第二个输入框时,第二个输入框则带着 `shouldFocus={false}` 的属性值。可以发现,即使你往两个输入框里都输入一些内容时,它们仍然能正常工作。
-You can't declare an Effect conditionally, but your Effect can include conditional logic.
+所以,不能有条件地声明 Effect,但 Effect 中可以包含条件逻辑。
-Put the conditional logic inside the Effect. You will need to specify `shouldFocus` as a dependency because you are using it inside the Effect. (This means that if some input's `shouldFocus` changes from `false` to `true`, it will focus after mount.)
+向 Effect 中加入条件逻辑。由于 Effect 使用了 `shouldFocus`,你需要为 Effect 指定 `shouldFocus` 这个依赖项。这也意味着如果输入框的 `shouldFocus` 由 `false` 变为 `true` 时,它才会在下次渲染时获得焦点。
@@ -1263,13 +1263,13 @@ export default function Form() {
const name = firstName + ' ' + lastName;
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} form
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}表单
{show && (
<>
- Enter your first name:
+ 输入你的名:
setFirstName(e.target.value)}
@@ -1277,14 +1277,14 @@ export default function Form() {
/>
- Enter your last name:
+ 输入你的姓:
setLastName(e.target.value)}
shouldFocus={false}
/>
- Hello, {upper ? name.toUpperCase() : name}
+ 你好,{upper ? name.toUpperCase() : name}
>
)}
>
@@ -1308,15 +1308,15 @@ body {
-#### Fix an interval that fires twice {/*fix-an-interval-that-fires-twice*/}
+#### 修复计时器触发两次的问题 {/*fix-an-interval-that-fires-twice*/}
-This `Counter` component displays a counter that should increment every second. On mount, it calls [`setInterval`.](https://developer.mozilla.org/zh-CN/docs/Web/API/setInterval) This causes `onTick` to run every second. The `onTick` function increments the counter.
+下面的 `Counter` 组件显示一个计数器,应该每秒递增一次。在组件挂载时,它调用 [`setInterval`](https://developer.mozilla.org/zh-CN/docs/Web/API/setInterval)。这会导致 `onTick` 每秒运行一次。`onTick` 函数会递增计数器。
-However, instead of incrementing once per second, it increments twice. Why is that? Find the cause of the bug and fix it.
+然而,计数器不是每秒递增一次,而是两次。这是为什么呢?找出错误的原因并修复它。
-Keep in mind that `setInterval` returns an interval ID, which you can pass to [`clearInterval`](https://developer.mozilla.org/zh-CN/docs/Web/API/clearInterval) to stop the interval.
+请记住,`setInterval` 返回一个 interval ID,你可以将其传递给 [`clearInterval`](https://developer.mozilla.org/zh-CN/docs/Web/API/clearInterval) 来停止计时。
@@ -1348,7 +1348,7 @@ export default function Form() {
const [show, setShow] = useState(false);
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} counter
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}计时器
{show && }
@@ -1373,11 +1373,11 @@ body {
-When [Strict Mode](/reference/react/StrictMode) is on (like in the sandboxes on this site), React remounts each component once in development. This causes the interval to be set up twice, and this is why each second the counter increments twice.
+在 [严格模式](/reference/react/StrictMode) 下,(本网站中的示例沙盒(sandbox)都已开启严格模式),React 在开发模式中,每个组件都会重复挂载一次。这也就导致计数器组件被挂载了两次。所以,计时器也被设立了两次,这就是为什么计数器每秒递增两次的原因。
-However, React's behavior is not the *cause* of the bug: the bug already exists in the code. React's behavior makes the bug more noticeable. The real cause is that this Effect starts a process but doesn't provide a way to clean it up.
+然而,这并不是 React 本身的错:而是 Effect 代码中本身就存在问题。React 只不过把这个问题放大了。真正的错误原因是这样的 Effect 启动后,但没有提供清理函数,所以上一次的 Effect 残留就没有被除去。
-To fix this code, save the interval ID returned by `setInterval`, and implement a cleanup function with [`clearInterval`](https://developer.mozilla.org/zh-CN/docs/Web/API/clearInterval):
+要修复这个问题,保存 `setInterval` 返回的 interval ID,并使用 [`clearInterval`](https://developer.mozilla.org/zh-CN/docs/Web/API/clearInterval) 实现一个清理函数:
@@ -1408,7 +1408,7 @@ export default function App() {
const [show, setShow] = useState(false);
return (
<>
- setShow(s => !s)}>{show ? 'Hide' : 'Show'} counter
+ setShow(s => !s)}>{show ? '隐藏' : '展示'}计时器
{show && }
@@ -1431,13 +1431,13 @@ body {
-In development, React will still remount your component once to verify that you've implemented cleanup well. So there will be a `setInterval` call, immediately followed by `clearInterval`, and `setInterval` again. In production, there will be only one `setInterval` call. The user-visible behavior in both cases is the same: the counter increments once per second.
+在开发环境中,React 仍然会重复挂载一次组件,通过放大问题,以确保已经正确地实现了清理函数。这样,调用一次 `setInterval` 后就紧接着调用 `clearInterval`,然后再调用 `setInterval`。在生产环境中与开发环境不同,React 只挂载一次组件,即只调用一次 `setInterval`。两种情况下用户感知的效果是相同的:计数器每秒递增一次。
-#### Fix fetching inside an Effect {/*fix-fetching-inside-an-effect*/}
+#### 修复在 Effect 中获取数据的问题 {/*fix-fetching-inside-an-effect*/}
-This component shows the biography for the selected person. It loads the biography by calling an asynchronous function `fetchBio(person)` on mount and whenever `person` changes. That asynchronous function returns a [Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) which eventually resolves to a string. When fetching is done, it calls `setBio` to display that string under the select box.
+下面这个组件显示所选人物的传记。它在挂载时和每当 `person` 改变时通过调用一个异步函数 `fetchBio(person)` 来加载传记。该异步函数返回一个 [Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise),最终解析为一个字符串。当获取完成时,它调用 `setBio` 以将该字符串显示在选择框下方。
@@ -1466,7 +1466,7 @@ export default function Page() {
Taylor
- {bio ?? 'Loading...'}
+ {bio ?? '加载中……'}
>
);
}
@@ -1477,7 +1477,7 @@ export async function fetchBio(person) {
const delay = person === 'Bob' ? 2000 : 200;
return new Promise(resolve => {
setTimeout(() => {
- resolve('This is ' + person + '’s bio.');
+ resolve('这是' + person + '的传记。');
}, delay);
})
}
@@ -1487,30 +1487,30 @@ export async function fetchBio(person) {
-There is a bug in this code. Start by selecting "Alice". Then select "Bob" and then immediately after that select "Taylor". If you do this fast enough, you will notice that bug: Taylor is selected, but the paragraph below says "This is Bob's bio."
+这段代码中有一个错误。试试首先选择 `Alice`,然后选择 `Bob`,然后紧接着选择 `Taylor`。如果操作得足够快,会注意到这个错误:Taylor 被选中了,但下面的一段却说:“这是Bob的传记。”
-Why does this happen? Fix the bug inside this Effect.
+为什么会发生这种情况?试着修复此 Effect 中的错误。
-If an Effect fetches something asynchronously, it usually needs cleanup.
+如果 Effect 需要异步获取某些数据,它通常需要清理函数。
-To trigger the bug, things need to happen in this order:
+触发问题时,程序的指令序列是这样的:
-- Selecting `'Bob'` triggers `fetchBio('Bob')`
-- Selecting `'Taylor'` triggers `fetchBio('Taylor')`
-- **Fetching `'Taylor'` completes *before* fetching `'Bob'`**
-- The Effect from the `'Taylor'` render calls `setBio('This is Taylor’s bio')`
-- Fetching `'Bob'` completes
-- The Effect from the `'Bob'` render calls `setBio('This is Bob’s bio')`
+- 选中 `'Bob'` 触发 `fetchBio('Bob')`
+- 选中 `'Taylor'` 触发 `fetchBio('Taylor')`
+- **在加载 `'Taylor'` 的数据完成之前,就已经加载完成了 `'Bob'` 的数据**
+- 加载 `'Taylor'` 数据的 Effect 调用了 `setBio('这是Taylor的传记')`
+- 加载完成 `'Bob'` 的数据
+- 加载 `'Bob'` 数据的 Effect 调用了 `setBio('这是Bob的传记')`
-This is why you see Bob's bio even though Taylor is selected. Bugs like this are called [race conditions](https://en.wikipedia.org/wiki/Race_condition) because two asynchronous operations are "racing" with each other, and they might arrive in an unexpected order.
+这就是为什么即使 Taylor 被选中了,但显示的仍然是 Bob 的数据。这种问题被称之为 [条件竞争](https://en.wikipedia.org/wiki/Race_condition),因为两个异步操作都在彼此竞争,谁先谁后是不可预期的。
-To fix this race condition, add a cleanup function:
+为了修复这种问题,在 Effect 中添加清理函数:
@@ -1544,7 +1544,7 @@ export default function Page() {
Taylor
- {bio ?? 'Loading...'}
+ {bio ?? '加载中……'}
>
);
}
@@ -1555,7 +1555,7 @@ export async function fetchBio(person) {
const delay = person === 'Bob' ? 2000 : 200;
return new Promise(resolve => {
setTimeout(() => {
- resolve('This is ' + person + '’s bio.');
+ resolve('这是' + person + '的传记。');
}, delay);
})
}
@@ -1564,16 +1564,16 @@ export async function fetchBio(person) {
-Each render's Effect has its own `ignore` variable. Initially, the `ignore` variable is set to `false`. However, if an Effect gets cleaned up (such as when you select a different person), its `ignore` variable becomes `true`. So now it doesn't matter in which order the requests complete. Only the last person's Effect will have `ignore` set to `false`, so it will call `setBio(result)`. Past Effects have been cleaned up, so the `if (!ignore)` check will prevent them from calling `setBio`:
+其实,每个 Effect 都可以在里面设置一个 `ignore` 标记变量。在最开始,`ignore` 被设置为 `false`。然而,当 Effect 执行清理函数后(就像你选中了列表中不同的人时),`ignore` 就会被设置为 `true`。所以此时请求完成的顺序并不重要。只有最后选中的人在执行它的 Effect 时,`ignore` 会被设为 `false`,所以它会调用 `setBio(result)`。而之前的 Effect 都被清理掉了。所以检查 `if (!ignore)` 会阻止调用 `setBio`:
-- Selecting `'Bob'` triggers `fetchBio('Bob')`
-- Selecting `'Taylor'` triggers `fetchBio('Taylor')` **and cleans up the previous (Bob's) Effect**
-- Fetching `'Taylor'` completes *before* fetching `'Bob'`
-- The Effect from the `'Taylor'` render calls `setBio('This is Taylor’s bio')`
-- Fetching `'Bob'` completes
-- The Effect from the `'Bob'` render **does not do anything because its `ignore` flag was set to `true`**
+- 选中 `'Bob'` 触发 `fetchBio('Bob')`
+- 选中 `'Taylor'` 触发 `fetchBio('Taylor')`,**然后清理之前加载(Bob)数据时的 Effect**
+- 在加载完 `'Bob'` 的数据 **之前**,就已经完成加载 `'Taylor'` 的数据
+- 渲染 `'Taylor'` 时的 Effect 调用 `setBio('这是Taylor的传记')`
+- 加载完成 `'Bob'` 的数据
+- 渲染 `'Bob'` 时的 Effect 不会做任何事情,因为 `ignore` 已经被设为了 `true`。
-In addition to ignoring the result of an outdated API call, you can also use [`AbortController`](https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController) to cancel the requests that are no longer needed. However, by itself this is not enough to protect against race conditions. More asynchronous steps could be chained after the fetch, so using an explicit flag like `ignore` is the most reliable way to fix this type of problems.
+除了忽略过时 API 调用的结果外,你还可以使用 [`AbortController`](https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController) 来取消不再需要的请求。然而,仅靠这个还不足以防止竞态条件。更多的异步步骤可能会在获取之后链接起来,因此使用显式标记,如 `ignore` 变量,是修复这类问题最可靠的方法。