有些 JavaScript 函数是纯函数。纯函数仅执行计算,仅此而已。通过严格地将你的组件编写为纯函数,你可以在代码库增长时避免一整类令人困惑的错误和不可预测的行为。但是,要获得这些好处,你必须遵守一些规则。

¥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.

你将学习到

  • 什么是纯粹以及它如何帮助你避免错误

    ¥What purity is and how it helps you avoid bugs

  • 如何通过将更改排除在渲染阶段之外来保持组件纯粹

    ¥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.

考虑这个数学公式:y = 2x

¥Consider this math formula: y = 2x.

如果 x = 2 那么 y = 4。始终如此。

¥If x = 2 then y = 4. Always.

如果 x = 3 那么 y = 6。始终如此。

¥If x = 3 then y = 6. Always.

如果 x = 3y 有时不会是 9–12.5 取决于一天中的时间或股票市场的状态。

¥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.

如果 y = 2xx = 3y 将始终为 6

¥If y = 2x and x = 3, y will always be 6.

如果我们把它做成一个 JavaScript 函数,它看起来像这样:

¥If we made this into a JavaScript function, it would look like this:

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

在上面的例子中,double 是一个纯函数。如果你传递 3,它将返回 6。始终如此。

¥In the above example, double is a pure function. If you pass it 3, it will return 6. Always.

React 就是围绕这个概念设计的。React 假定你编写的每个组件都是纯函数。这意味着你编写的 React 组件必须始终在给定相同输入的情况下返回相同的 JSX:

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

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

当你将 drinkers={2} 传递给 Recipe 时,它会返回包含 2 cups of water 的 JSX。始终如此。

¥When you pass drinkers={2} to Recipe, it will return JSX containing 2 cups of water. Always.

如果传递 drinkers={4},它将返回包含 4 cups of water 的 JSX。始终如此。

¥If you pass drinkers={4}, it will return JSX containing 4 cups of water. Always.

就像一个数学公式。

¥Just like a math formula.

你可以将你的组件视为教程:如果你跟着它们做,在烹饪过程中不引入新的食材,你每次都会得到同样的菜。那个 “碟子” 就是组件服务于 React 渲染 的 JSX。

¥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.

A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk

Illustrated by Rachel Lee Nabors

副作用:(非)预期的后果

¥Side Effects: (un)intended consequences

React 的渲染过程必须始终是纯粹的。组件应该只返回它们的 JSX,而不应该更改渲染之前存在的任何对象或变量 - 这会使它们变得不纯粹!

¥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!

这是一个打破这条规则的组件:

¥Here is a component that breaks this rule:

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

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

该组件正在读取和写入在其外部声明的 guest 变量。这意味着多次调用该组件将产生不同的 JSX!更重要的是,如果其他组件读取 guest,它们也会生成不同的 JSX,具体取决于它们的渲染时间!这是不可预测的。

¥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.

回到我们的公式 y = 2x,现在即使 x = 2,我们不能相信 y = 4。我们的测试可能会失败,我们的用户会感到困惑,飞机会从天上掉下来 - 你可以看到这将如何导致令人困惑的错误!

¥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!

你可以通过 guest 作为属性传递 修复此组件:

¥You can fix this component by passing guest as a prop instead:

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

现在你的组件是纯粹的,因为它返回的 JSX 只依赖于 guest 属性。

¥Now your component is pure, as the JSX it returns only depends on the guest prop.

通常,你不应期望你的组件以任何特定顺序渲染。在 y = 5x 之前或之后调用 y = 2x 都没有关系:这两个公式将相互独立解决。同理,每个组件都应该只 “为自己考虑”,在渲染过程中不要试图去协调或依赖其他组件。渲染就像学校考试:每个组件都应该自己计算 JSX!

¥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!

深入研究

使用严格模式检测不纯计算

¥Detecting impure calculations with StrictMode

尽管你可能还没有使用过它们,但在 React 中,你可以在渲染时读取三种输入:属性状态上下文。你应该始终将这些输入视为只读。

¥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.

当你想要更改某些内容以响应用户输入时,你应该 设置状态 而不是写入变量。当你的组件正在渲染时,你不应该改变预先存在的变量或对象。

¥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 提供了一个 “严格模式”,它在开发过程中两次调用每个组件的函数。通过两次调用组件函数,严格模式有助于找到违反这些规则的组件。

¥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.

请注意原始示例如何显示 “Guest #2”、“Guest #4” 和 “Guest #6” 而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原来的函数是不纯的,所以调用它两次就坏了。但是即使每次调用该函数两次,固定的纯版本也能正常工作。纯函数仅进行计算,因此调用它们两次不会改变任何内容 - 就像调用 double(2) 两次不会改变返回的内容一样,求解 y = 2x 两次不会改变 y 的内容。相同的输入,相同的输出。始终如此。

¥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.

严格模式对生产没有影响,因此它不会降低用户的应用速度。要选择进入严格模式,你可以将根组件封装到 <React.StrictMode> 中。一些框架默认这样做。

¥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.

局部突变:你组件的小秘密

¥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!

但是,在渲染时更改刚刚创建的变量和对象是完全可以的。在此示例中,你创建一个 [] 数组,将其分配给 cups 变量,然后将 push 十几个杯子放入其中:

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

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;
}

如果 cups 变量或 [] 数组是在 TeaGathering 函数之外创建的,这将是一个巨大的问题!你将通过将条目推入该数组来更改预先存在的对象。

¥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.

但是,这很好,因为你是在同一个渲染过程中在 TeaGathering 中创建它们的。TeaGathering 之外的任何代码都不会知道这发生了。这称为 “局部突变” — 它就像你组件的小秘密。

¥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.

哪些地方可能会引起副作用

¥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.

在 React 中,副作用通常属于 事件处理程序 事件处理程序,是当你执行某些操作(例如,当你单击按钮时)时 React 运行的函数。即使事件处理程序是在你的组件中定义的,它们也不会在渲染期间运行!所以事件处理程序不需要是纯粹的。

¥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.

如果你已用尽所有其他选项并且找不到适合你的副作用的事件处理程序,你仍然可以通过在组件中调用 useEffect 将其附加到返回的 JSX。这告诉 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.

如果可能,请尝试仅通过渲染来表达你的逻辑。你会惊讶于这能带你走多远!

¥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.

  • 如果在渲染深层组件树的过程中某些数据发生变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹使你可以安全地随时停止计算。

    ¥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 功能都利用了纯粹。从数据获取到动画再到性能,保持组件纯粹释放了 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.

回顾

  • 组件必须是纯粹的,这意味着:

    ¥A component must be pure, meaning:

    • 它只管自己的事。它不应更改渲染前存在的任何对象或变量。

      ¥It minds its own business. It should not change any objects or variables that existed before rendering.

    • 相同的输入,相同的输出。给定相同的输入,组件应该始终返回相同的 JSX。

      ¥Same inputs, same output. Given the same inputs, a component should always return the same JSX.

  • 渲染可以随时发生,因此组件不应依赖于彼此的渲染顺序。

    ¥Rendering can happen at any time, so components should not depend on each others’ rendering sequence.

  • 你不应该改变你的组件用于渲染的任何输入。这包括属性、状态和上下文。要更新屏幕,“set” 状态 而不是改变预先存在的对象。

    ¥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.

  • 努力在返回的 JSX 中表达组件的逻辑。当你需要 “改变东西” 时,你通常希望在事件处理程序中进行。不得已,你可以 useEffect

    ¥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.

  • 编写纯函数需要一些练习,但它释放了 React 范例的力量。

    ¥Writing pure functions takes a bit of practice, but it unlocks the power of React’s paradigm.

挑战 1 / 3:
修复损坏的时钟

¥Fix a broken clock

该组件尝试在午夜到早上六点之间将 <h1> 的 CSS 类设置为 "night",并在所有其他时间设置为 "day"。但是,它不起作用。你能修复这个组件吗?

¥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?

你可以通过临时更改计算机的时区来验证你的解决方案是否有效。当当前时间在午夜和早上六点之间时,时钟应该有反色!

¥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>
  );
}


React 中文网 - 粤ICP备13048890号