保持组件的纯洁性

一些 JavaScript 函数是纯函数。 纯函数只执行计算,不做其他任何事情。通过严格地将组件编写为纯函数,你可以避免随着代码库的增长而出现一整类令人困惑的错误和不可预测的行为。然而,要获得这些好处,你必须遵循一些规则。

你将学习

  • 什么是纯洁性以及它如何帮助你避免错误
  • 如何通过将更改排除在渲染阶段之外来保持组件的纯洁性
  • 如何使用严格模式查找组件中的错误

纯洁性:组件作为公式

在计算机科学中(尤其是在函数式编程领域),纯函数 是指具有以下特征的函数:

  • 它只关注自身。 它不会改变调用它之前存在的任何对象或变量。
  • 相同的输入,相同的输出。 给定相同的输入,纯函数应始终返回相同的结果。

你可能已经熟悉一个纯函数的例子:数学公式。

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

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

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

如果 x = 3y 不会根据时间或股市状况有时是 9,有时是 –1,有时是 2.5

如果 y = 2x 并且 x = 3,那么 y始终6

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

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

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

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

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

如果你传递 drinkers={4},它将返回包含 4 杯水 的 JSX。始终如此。

就像数学公式一样。

你可以将你的组件想象成食谱:如果你按照食谱操作,并且在烹饪过程中不引入新的食材,那么你每次都会得到相同的菜肴。这个“菜肴”就是组件提供给 React 进行 渲染 的 JSX。

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

插图作者 Rachel Lee Nabors

副作用:(非)预期后果

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

下面是一个违反此规则的组件

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!这是不可预测的。

回到我们的公式 y = 2x,现在即使 x = 2,我们也不能确定 y = 4。我们的测试可能会失败,我们的用户会感到困惑,飞机可能会从天上掉下来——你可以看到这会导致多么混乱的错误!

你可以通过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} />
    </>
  );
}

现在你的组件是纯的,因为它返回的 JSX 只取决于 guest prop。

通常,你不应该期望你的组件以任何特定的顺序渲染。在 y = 5x 之前还是之后调用 y = 2x 并不重要:这两个公式将彼此独立地解析。同样,每个组件应该只“为自己着想”,而不是试图在渲染期间与其他组件协调或依赖其他组件。渲染就像学校考试:每个组件都应该自己计算 JSX!

深入探讨

使用 StrictMode 检测不纯计算

虽然你可能还没有全部使用过,但在 React 中,你可以在渲染时读取三种输入:propsstatecontext。你应该始终将这些输入视为只读的。

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

React 提供了一种“严格模式”,在这种模式下,它会在开发过程中调用每个组件的函数两次。通过调用组件函数两次,严格模式可以帮助找到违反这些规则的组件。

请注意,原始示例显示的是“Guest #2”、“Guest #4”和“Guest #6”,而不是“Guest #1”、“Guest #2”和“Guest #3”。原始函数是不纯的,因此调用两次会破坏它。但是,即使每次调用函数两次,修复后的纯版本也能正常工作。纯函数只进行计算,因此调用两次不会改变任何东西——就像调用 double(2) 两次不会改变返回的值一样,求解 y = 2x 两次不会改变 y 的值。相同的输入,相同的输出。始终如此。

严格模式在生产环境中不起作用,因此它不会降低应用程序对用户的速度。要选择使用严格模式,你可以将根组件包装到 <React.StrictMode> 中。某些框架默认情况下会这样做。

局部突变:组件的小秘密

在上面的例子中,问题在于组件在渲染时改变了一个预先存在的变量。这通常被称为“突变”,使它听起来更可怕一些。纯函数不会改变函数作用域之外的变量或调用之前创建的对象——这会使它们变得不纯!

然而,在渲染时改变你刚刚创建的变量和对象是完全没有问题的。在这个例子中,你创建了一个 [] 数组,将其赋值给一个 cups 变量,然后 push 一打杯子进去

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 函数外部创建的,那将是一个大问题!你将通过将项目推送到该数组中来更改预先存在的对象。

然而,这很好,因为你是在 TeaGathering 内部在同一次渲染期间创建它们的。 TeaGathering 之外的任何代码都不会知道发生了这种事。这被称为“局部突变”——就像组件的小秘密。

哪里可以引起副作用

虽然函数式编程严重依赖于纯度,但在某些时候,某些地方,某些东西必须改变。这就是编程的意义所在!这些变化——更新屏幕、启动动画、更改数据——被称为副作用。它们是“在旁边”发生的事情,而不是在渲染过程中。

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

如果你已经用尽了所有其他选项,并且找不到适合你副作用的事件处理程序,你仍然可以使用组件中的 useEffect 调用将其附加到返回的 JSX。这告诉 React 在渲染之后、允许副作用时再执行它。但是,这种方法应该是你的最后手段。

如果可能,请尝试仅使用渲染来表达你的逻辑。你会惊讶于它能把你带多远!

深入探讨

React 为什么关注纯度?

编写纯函数需要养成一些习惯并遵守一些规范,但它也解锁了奇妙的可能性。

  • 你的组件可以在不同的环境中运行,例如在服务器上!由于它们对相同的输入返回相同的结果,一个组件可以服务于许多用户请求。
  • 你可以通过跳过渲染输入没有改变的组件来提高性能。这样做是安全的,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。
  • 如果在渲染深层组件树的过程中某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯度使得在任何时候停止计算都是安全的。

我们正在构建的每个新 React 功能都利用了纯度的优势。从数据获取到动画再到性能,保持组件的纯度可以释放 React 范式的强大功能。

总结

  • 组件必须是纯的,这意味着
    • 它只管自己的事。它不应该改变渲染之前存在的任何对象或变量。
    • 相同的输入,相同的输出。给定相同的输入,组件应该总是返回相同的 JSX。
  • 渲染可以随时发生,所以组件不应该依赖于彼此的渲染顺序。
  • 你不应该改变组件用于渲染的任何输入。这包括 props、状态和上下文。要更新屏幕,请“设置”状态,而不是改变预先存在的对象。
  • 尽量在返回的 JSX 中表达组件的逻辑。当你需要“改变东西”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect
  • 编写纯函数需要一些练习,但它可以释放 React 范式的强大功能。

挑战 1 3:
修复一个坏掉的时钟

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

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

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