保持组件纯粹
一些 JavaScript 函数是纯函数。 纯函数仅执行计算,仅此而已。 通过严格地将您的组件编写为纯函数,您可以在代码库增长时避免一整类令人困惑的错误和不可预测的行为。 但是,要获得这些好处,您必须遵守一些规则。
你将学习
- 什么是纯粹以及它如何帮助您避免错误
- 如何通过将更改排除在渲染阶段之外来保持组件纯粹
- 如何使用严格模式查找组件中的错误
纯粹:作为公式的组件
在计算机科学(尤其是函数式编程领域)中,纯函数是具有以下特征的函数:
- 它只管自己的事。 它不会更改调用之前存在的任何对象或变量。
- 相同的输入,相同的输出。 给定相同的输入,纯函数应该总是返回相同的结果。
您可能已经熟悉纯函数的一个示例:数学中的公式。
考虑这个数学公式:y = 2x。
如果 x = 2 则 y = 4。始终如此。
如果 x = 3 则 y = 6。始终如此。
如果 x = 3,则 y 不会时而是 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。
副作用:(非)预期的后果
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 />
</>
);
}
该组件正在读取和写入在其外部声明的来宾变量。 这意味着多次调用这个组件会产生不同的 JSX! 更重要的是,如果其他组件读取 guest,它们也会产生不同的 JSX,这取决于它们被渲染的时间! 这是不可预测的。
回到我们的公式 y = 2x,现在即使 x = 2,我们也不能相信 y = 4。我们的测试可能会失败,我们的用户会感到困惑,飞机会从天上掉下来——你可以看到这将如何导致混淆错误!
您可以通过将 guest 作为props传递来修复此组件:
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属性。
通常,您不应期望您的组件以任何特定顺序呈现。 在 y = 5x 之前或之后调用 y = 2x 并不重要:两个公式将彼此独立地解析。 同样,每个组件都应该只“为自己思考”,而不是试图在渲染过程中与其他组件进行协调或依赖。 渲染就像学校考试:每个组件都应该自己计算 JSX!
深度阅读:使用 StrictMode 检测不纯粹的计算
尽管您可能还没有使用过它们,但在 React 中,您可以在渲染时读取三种输入:props、state 和 context。 您应该始终将这些输入视为只读。
当您想要更改某些内容以响应用户输入时,您应该设置状态而不是写入变量。 当你的组件正在渲染时,你不应该改变预先存在的变量或对象。
React 提供了一种“Strict Mode”,在这种模式下,它在开发过程中会调用每个组件的函数两次。 通过两次调用组件函数,严格模式有助于找到违反这些规则的组件。
请注意原始示例是如何显示“Guest #2”、“Guest #4”和“Guest #6”而不是“Guest #1”、“Guest #2”和“Guest #3”的。 原来的函数是不纯的,所以调用它两次就坏了。 但是即使每次调用该函数两次,固定的纯版本也能正常工作。 纯函数只计算,所以调用它们两次不会改变任何东西——就像调用 double(2) 两次不会改变返回的内容,求解 y = 2x 两次不会改变 y 是什么一样。 相同的输入,相同的输出。 总是。
严格模式对生产没有影响,因此它不会降低用户的应用程序速度。 要选择进入严格模式,您可以将根组件包装到 <React.StrictMode> 中。 一些框架默认这样做。
局部突变:组件的小秘密
在上面的示例中,问题是组件在渲染时更改了一个预先存在的变量。 这通常被称为“突变”,以使其听起来更可怕。 纯函数不会改变函数范围之外的变量或在调用之前创建的对象——这使得它们不纯!
但是,在渲染时更改刚刚创建的变量和对象是完全可以的。 在此示例中,您创建一个 [] 数组,将其分配给一个 cups 变量,然后将一打杯子放入其中:
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;
}
如果在 TeaGathering 函数之外创建 cups 变量或 [] 数组,这将是一个巨大的问题! 您将通过将项目推入该数组来更改预先存在的对象。
但是,这里很好,因为您是在同一渲染过程中在 TeaGathering 中创建它们的。 TeaGathering 之外的任何代码都不会知道这发生了。 这称为“局部突变”——它就像是您组件的小秘密。
哪里会引起副作用
虽然函数式编程在很大程度上依赖于纯度,但在某个时候,某个地方,某些东西必须改变。 这就是编程的意义所在! 这些更改——更新屏幕、启动动画、更改数据——称为副作用。 它们是“在旁边”发生的事情,而不是在渲染过程中。
在 React 中,副作用通常属于事件处理程序。 事件处理程序是 React 在您执行某些操作时运行的函数——例如,当您单击按钮时。 即使事件处理程序是在组件内部定义的,它们也不会在渲染期间运行! 所以事件处理程序不需要是纯粹的。
如果您已经用尽了所有其他选项并且找不到适合您的副作用的事件处理程序,您仍然可以通过在组件中调用 useEffect 将其附加到返回的 JSX。 这告诉 React 在渲染后允许副作用时稍后执行它。 但是,这种方法应该是您最后的选择。
如果可能,请尝试仅通过渲染来表达您的逻辑。 你会惊讶于这能带你走多远!
深度阅读:为什么 React 关心纯度?
编写纯函数需要一些习惯和纪律。 但它也开启了奇妙的机会:
- 您的组件可以在不同的环境中运行——例如,在服务器上! 由于它们对相同的输入返回相同的结果,因此一个组件可以满足许多用户请求。
- 您可以通过跳过输入未更改的渲染组件来提高性能。 这是安全的,因为纯函数总是返回相同的结果,所以它们可以安全地缓存。
- 如果在渲染深层组件树的过程中某些数据发生变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。 纯度使您可以安全地随时停止计算。
我们正在构建的每个新 React 功能都利用了纯度。 从数据获取到动画再到性能,保持组件纯粹解锁了 React 范式的力量。
回顾
- 组件必须是纯的,这意味着:
-
它只管自己的事
。 它不应更改渲染前存在的任何对象或变量。 -
相同的输入,相同的输出
。 给定相同的输入,组件应该始终返回相同的 JSX。
-
- 渲染可以随时发生,因此组件不应依赖于彼此的渲染顺序。
- 你不应该改变你的组件用于渲染的任何输入。 这包括props、state和context。 要更新屏幕,“set”状态而不是改变预先存在的对象。
- 努力在返回的 JSX 中表达组件的逻辑。 当您需要“改变事物”时,您通常希望在事件处理程序中进行。 不得已,您可以使用 Effect。
- 编写纯函数需要一些练习,但它解锁了 React 范式例的力量。
网友评论