一些 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。
插图作者 Rachel Lee Nabors
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 = 2x 之前还是之后调用 y = 5x 都没有关系:这两个公式将独立于彼此计算。同样,每个组件都应该“独立思考”,并且在渲染过程中不尝试与其他组件协调或依赖于其他组件。渲染就像学校考试:每个组件都应该独立计算JSX!
深入探讨
虽然你可能还没有全部使用过它们,但在React中,有三种类型的输入可以在渲染时读取:props、state和context。你应该始终将这些输入视为只读。
当你想根据用户输入更改某些内容时,你应该设置state,而不是写入变量。在组件渲染过程中,你决不应该更改预先存在的变量或对象。
React提供了一种“StrictMode”,在开发过程中,它会两次调用每个组件的函数。通过两次调用组件函数,StrictMode有助于查找违反这些规则的组件。
请注意,原始示例显示“访客#2”、“访客#4”和“访客#6”,而不是“访客#1”、“访客#2”和“访客#3”。原始函数是不纯的,因此两次调用它会破坏它。但是,即使每次都调用该函数两次,修复后的纯版本也能正常工作。纯函数只进行计算,因此多次调用它们不会改变任何结果——就像两次调用double(2)
不会改变返回的结果一样,两次求解y = 2x也不会改变y的值。相同的输入,相同的输出。始终如此。
StrictMode在生产环境中没有影响,因此不会减慢用户的应用程序速度。要启用StrictMode,你可以将根组件包装到<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范式的强大功能。
回顾
- 组件必须是纯的,这意味着
- 它只管自己的事。它不应该更改渲染前存在的任何对象或变量。
- 相同的输入,相同的输出。给定相同的输入,组件应该始终返回相同的 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> ); }