保持组件纯粹 | Keeping Components Pure

Some JavaScript functions are pure. Pure functions only perform a calculation and nothing more. By strictly only writing your components as pure functions, you can avoid an entire class of baffling bugs and unpredictable behavior as your codebase grows. To get these benefits, though, there are a few rules you must follow. 部分 JavaScript 函数是 纯粹 的,这类函数通常被称为纯函数。纯函数仅执行计算操作,不做其他操作。你可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的 bug 以及不可预测的行为。但为了获得这些好处,你需要遵循一些规则。

你将会学习到

  • What purity is and how it helps you avoid bugs
  • 纯函数是什么,以及它如何帮助你避免 bug
  • How to keep components pure by keeping changes out of the render phase
  • 如何将数据变更与渲染过程分离,以保持组件的纯粹
  • How to use Strict Mode to find mistakes in your components
  • 如何使用严格模式发现组件中的错误

纯函数:组件作为公式 | Purity: Components as formulas

In computer science (and especially the world of functional programming), a pure function is a function with the following characteristics: 在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:

  • It minds its own business. It does not change any objects or variables that existed before it was called.
  • 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
  • Same inputs, same output. Given the same inputs, a pure function should always return the same result.
  • 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。

You might already be familiar with one example of pure functions: formulas in math. 举个你非常熟悉的纯函数示例:数学中的公式。

Consider this math formula: y = 2x. 考虑如下数学公式:y = 2x

If x = 2 then y = 4. Always. 若 x = 2y = 4。永远如此。

If x = 3 then y = 6. Always. 若 x = 3y = 6。永远如此。

If x = 3, y won’t sometimes be 9 or –1 or 2.5 depending on the time of day or the state of the stock market. 若 x = 3,那么 y 并不会因为时间或股市的影响,而有时等于 9–12.5

If y = 2x and x = 3, y will always be 6. 若 y = 2xx = 3, 那么 y 永远 等于 6.

If we made this into a JavaScript function, it would look like this: 我们使用 JavaScript 的函数实现,看起来将会是这样:

function double(number) {
return 2 * number;
}

In the above example, double is a pure function. If you pass it 3, it will return 6. Always. 上述例子中,double() 就是一个 纯函数。如果你传入 3 ,它将总是返回 6

React is designed around this concept. React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs: React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

When you pass drinkers={2} to Recipe, it will return JSX containing 2 cups of water. Always. 当你给函数 Recipe 传入 drinkers={2} 参数时,它将返回包含 2 cups of water 的 JSX。永远如此。

If you pass drinkers={4}, it will return JSX containing 4 cups of water. Always. 而当你传入 drinkers={4} 时,它将返回包含 4 cups of water 的 JSX。永远如此。

Just like a math formula. 就像数学公式一样。

You could think of your components as recipes: if you follow them and don’t introduce new ingredients during the cooking process, you will get the same dish every time. That “dish” is the JSX that the component serves to React to render. 你可以把你的组件当作食谱:如果你遵循它们,并且在烹饪过程中不引入新食材,你每次都会得到相同的菜肴。那这道 “菜肴” 就是组件用于 React 渲染 的 JSX。

副作用:(不符合)预期的后果 | Side Effects: (un)intended consequences

React’s rendering process must always be pure. Components should only return their JSX, and not change any objects or variables that existed before rendering—that would make them impure! React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX,而不 改变 在渲染前,就已存在的任何对象或变量 — 这将会使它们变得不纯粹!

Here is a component that breaks this rule: 以下是违反这一规则的组件示例:

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  // Bad:正在更改预先存在的变量!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

This component is reading and writing a guest variable declared outside of it. This means that calling this component multiple times will produce different JSX! And what’s more, if other components read guest, they will produce different JSX, too, depending on when they were rendered! That’s not predictable. 该组件正在读写其外部声明的 guest 变量。这意味着 多次调用这个组件会产生不同的 JSX!并且,如果 其他 组件读取 guest ,它们也会产生不同的 JSX,其结果取决于它们何时被渲染!这是无法预测的。

Going back to our formula y = 2x, now even if x = 2, we cannot trust that y = 4. Our tests could fail, our users would be baffled, planes would fall out of the sky—you can see how this would lead to confusing bugs! 回到我们的公式 y = 2x ,现在即使 x = 2 ,我们也不能相信 y = 4 。我们的测试可能会失败,我们的用户可能会感到困扰,飞机可能会从天空坠毁——你将看到这会引发多么扑朔迷离的 bugs!

You can fix this component by passing guest as a prop instead: 你可以 guest 作为 prop 传入 来修复此组件:

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Now your component is pure, as the JSX it returns only depends on the guest prop. 现在你的组件就是纯粹的,因为它返回的 JSX 只依赖于 guest prop。

In general, you should not expect your components to be rendered in any particular order. It doesn’t matter if you call y = 2x before or after y = 5x: both formulas will resolve independently of each other. In the same way, each component should only “think for itself”, and not attempt to coordinate with or depend upon others during rendering. Rendering is like a school exam: each component should calculate JSX on their own! 一般来说,你不应该期望你的组件以任何特定的顺序被渲染。调用 y = 5xy = 2x 的先后顺序并不重要:这两个公式相互独立。同样地,每个组件也应该“独立思考”,而不是在渲染过程中试图与其他组件协调,或者依赖于其他组件。渲染过程就像是一场学校考试:每个组件都应该自己计算 JSX!

深入探讨

使用严格模式检测不纯的计算 | Detecting impure calculations with StrictMode

Although you might not have used them all yet, in React there are three kinds of inputs that you can read while rendering: props, state, and context. You should always treat these inputs as read-only. 尽管你可能还没使用过,但在 React 中,你可以在渲染时读取三种输入:propsstatecontext。你应该始终将这些输入视为只读。

When you want to change something in response to user input, you should set state instead of writing to a variable. You should never change preexisting variables or objects while your component is rendering. 当你想根据用户输入 更改 某些内容时,你应该 设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。

React offers a “Strict Mode” in which it calls each component’s function twice during development. By calling the component functions twice, Strict Mode helps find components that break these rules. React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件

Notice how the original example displayed “Guest #2”, “Guest #4”, and “Guest #6” instead of “Guest #1”, “Guest #2”, and “Guest #3”. The original function was impure, so calling it twice broke it. But the fixed pure version works even if the function is called twice every time. Pure functions only calculate, so calling them twice won’t change anything—just like calling double(2) twice doesn’t change what’s returned, and solving y = 2x twice doesn’t change what y is. Same inputs, same outputs. Always. 我们注意到,原始示例显示的是 “Guest #2”、“Guest #4” 和 “Guest #6”,而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即使调用该函数两次也能得到正确结果。纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2) 并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。

Strict Mode has no effect in production, so it won’t slow down the app for your users. To opt into Strict Mode, you can wrap your root component into <React.StrictMode>. Some frameworks do this by default. 严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode> 包裹根组件。一些框架会默认这样做。

局部 mutation:组件的小秘密 | Local mutation: Your component’s little secret

In the above example, the problem was that the component changed a preexisting variable while rendering. This is often called a “mutation” to make it sound a bit scarier. Pure functions don’t mutate variables outside of the function’s scope or objects that were created before the call—that makes them impure! 上述示例的问题出在渲染过程中,组件改变了 预先存在的 变量的值。为了让它听起来更可怕一点,我们将这种现象称为 突变(mutation) 。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象——这会使函数变得不纯粹!

However, it’s completely fine to change variables and objects that you’ve just created while rendering. In this example, you create an [] array, assign it to a cups variable, and then push a dozen cups into it: 但是,你完全可以在渲染时更改你 刚刚 创建的变量和对象。在本示例中,你创建一个 [] 数组,将其分配给一个 cups 变量,然后 push 一打 cup 进去:

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

If the cups variable or the [] array were created outside the TeaGathering function, this would be a huge problem! You would be changing a preexisting object by pushing items into that array. 如果 cups 变量或 [] 数组是在 TeaGathering 函数之外创建的,这将是一个很大的问题!因为如果那样的话,当你调用数组的 push 方法时,就会更改 预先存在的 对象。

However, it’s fine because you’ve created them during the same render, inside TeaGathering. No code outside of TeaGathering will ever know that this happened. This is called “local mutation”—it’s like your component’s little secret. 但是,这里不会有影响,因为每次渲染时,你都是在 TeaGathering 函数内部创建的它们。TeaGathering 之外的代码并不会知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。

哪些地方 可能 引发副作用 | Where you can cause side effects

While functional programming relies heavily on purity, at some point, somewhere, something has to change. That’s kind of the point of programming! These changes—updating the screen, starting an animation, changing the data—are called side effects. They’re things that happen “on the side”, not during rendering. 函数式编程在很大程度上依赖于纯函数,但 某些事物 在特定情况下不得不发生改变。这是编程的要义!这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关。

In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some action—for example, when you click a button. Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure. 在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数

If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort. 如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect 方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段

When possible, try to express your logic with rendering alone. You’ll be surprised how far this can take you! 如果可能,请尝试仅通过渲染过程来表达你的逻辑。你会惊讶于这能带给你多少好处!

深入探讨

React 为何侧重于纯函数? | Why does React care about purity?

Writing pure functions takes some habit and discipline. But it also unlocks marvelous opportunities: 编写纯函数需要遵循一些习惯和规程。但它开启了绝妙的机遇:

  • Your components could run in a different environment—for example, on the server! Since they return the same result for the same inputs, one component can serve many user requests.
  • 你的组件可以在不同的环境下运行 — 例如,在服务器上!由于它们针对相同的输入,总是返回相同的结果,因此一个组件可以满足多个用户请求。
  • You can improve performance by skipping rendering components whose inputs have not changed. This is safe because pure functions always return the same results, so they are safe to cache.
  • 你可以为那些输入未更改的组件来 跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。
  • If some data changes in the middle of rendering a deep component tree, React can restart rendering without wasting time to finish the outdated render. Purity makes it safe to stop calculating at any time.
  • 如果在渲染深层组件树的过程中,某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算。

Every new React feature we’re building takes advantage of purity. From data fetching to animations to performance, keeping components pure unlocks the power of the React paradigm. 我们正在构建的每个 React 新特性都利用到了纯函数。从数据获取到动画再到性能,保持组件的纯粹可以充分释放 React 范式的能力。

摘要

  • A component must be pure, meaning:
  • 一个组件必须是纯粹的,就意味着:
    • It minds its own business. It should not change any objects or variables that existed before rendering.
    • 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
    • Same inputs, same output. Given the same inputs, a component should always return the same JSX.
    • 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
  • Rendering can happen at any time, so components should not depend on each others’ rendering sequence.
  • 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
  • You should not mutate any of the inputs that your components use for rendering. That includes props, state, and context. To update the screen, “set” state instead of mutating preexisting objects.
  • 你不应该改变任何用于组件渲染的输入。这包括 props、state 和 context。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
  • Strive to express your component’s logic in the JSX you return. When you need to “change things”, you’ll usually want to do it in an event handler. As a last resort, you can useEffect.
  • 努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect
  • Writing pure functions takes a bit of practice, but it unlocks the power of React’s paradigm.
  • 编写纯函数需要一些练习,但它充分释放了 React 范式的能力。

1挑战 3 个挑战:
修复坏掉的时钟 | Fix a broken clock

This component tries to set the <h1>’s CSS class to "night" during the time from midnight to six hours in the morning, and "day" at all other times. However, it doesn’t work. Can you fix this component? 该组件尝试在午夜到早上 6 点期间,将 <h1> 的 CSS 类设置为 "night",而在其他时间都设置为 "day"。但它不起作用。你能修复这个组件吗?

You can verify whether your solution works by temporarily changing the computer’s timezone. When the current time is between midnight and six in the morning, the clock should have inverted colors! 你可以临时更改计算机的时区来验证你的解决方案是否有效。当前时间位于午夜至早上六点之间时,时钟应该有相反的颜色!

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}