一些 JavaScript 函数是纯函数。纯函数只进行计算,不做其他任何事情。只要你严格把组件写成纯函数,随着代码库不断增长,你就可以避免一整类令人困惑的 bug 和不可预测的行为。不过,要获得这些好处,你必须遵守一些规则。
You will learn
- 什么是纯净性,以及它如何帮助你避免 bug
- 如何通过避免在渲染阶段进行更改来保持组件纯净
- 如何使用 Strict Mode 在组件中发现错误
纯净性:把组件当作公式来看待
在计算机科学中(尤其是在函数式编程的世界里),纯函数是具有以下特征的函数:
- 各司其职。 它不会改变在它被调用之前就已存在的任何对象或变量。
- 相同的输入,相同的输出。 在相同输入下,纯函数应始终返回相同结果。
你可能已经熟悉纯函数的一个例子:数学公式。
考虑这个数学公式: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>煮 {drinkers} 杯水。</li> <li>加入 {drinkers} 勺茶和 {0.5 * drinkers} 勺香料。</li> <li>加入 {0.5 * drinkers} 杯牛奶煮沸,并按口味加糖。</li> </ol> ); } export default function App() { return ( <section> <h1>香料奶茶配方</h1> <h2>两人份</h2> <Recipe drinkers={2} /> <h2>聚会份</h2> <Recipe drinkers={4} /> </section> ); }
当你向 Recipe 传入 drinkers={2} 时,它会返回包含 2 cups of water 的 JSX。始终如此。
如果你传入 drinkers={4},它会返回包含 4 cups of water 的 JSX。始终如此。
就像数学公式一样。
你可以把组件看作食谱:如果你按照它来做,并且在烹饪过程中不加入新食材,那么每次都会得到同样的菜肴。这个“菜肴”就是组件提供给 React 用于渲染的 JSX。

Illustrated by Rachel Lee Nabors
副作用:有意或无意的后果
React 的渲染过程必须始终保持纯净。组件应该只返回它们的 JSX,而不应该改变渲染之前已经存在的任何对象或变量——那样会让它们变成不纯的!
下面是一个违反这条规则的组件:
let guest = 0; function Cup() { // 坏:修改了一个先前已经存在的变量! 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。我们的测试可能会失败,用户会感到困惑,飞机会从天上掉下来——你可以看出这会导致多么混乱的 bug!
你可以通过改为将 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!
Deep Dive
虽然你可能还没有全部用过,但在 React 中,渲染时可以读取三种输入:props、state 和 context。你应该始终把这些输入视为只读。
当你想要在响应用户输入时更改某些内容时,你应该设置 state,而不是写入某个变量。你永远不应该在组件渲染时更改先前已经存在的变量或对象。
React 提供了一个 “Strict Mode”,在开发环境中它会把每个组件函数调用两次。通过调用组件函数两次,Strict Mode 可以帮助发现违反这些规则的组件。
注意原始示例显示的是 “Guest #2”、“Guest #4” 和 “Guest #6”,而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原始函数是不纯的,所以调用两次就把它搞坏了。但修复后的纯版本即使每次都调用两次也能正常工作。纯函数只做计算,所以调用两次不会改变任何东西——就像调用 double(2) 两次不会改变返回值,以及解 y = 2x 两次也不会改变 y 的值一样。相同输入,相同输出。始终如此。
Strict Mode 在生产环境中没有影响,因此不会拖慢用户的应用。要启用 Strict Mode,你可以把根组件包裹在 <React.StrictMode> 中。有些框架默认就这么做。
局部突变:你组件的小秘密
在上面的例子中,问题在于组件在渲染期间修改了一个先前已经存在的变量。这通常被称为**“突变”**,这样听起来会更可怕一点。纯函数不会修改函数作用域之外的变量,或在调用之前创建的对象——那会让它们变得不纯!
然而,**在渲染时修改你刚刚创建的变量和对象是完全可以的。**在这个例子中,你创建了一个 [] 数组,把它赋给 cups 变量,然后向其中 push 了十几个杯子:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaGathering() { const 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 在渲染之后、允许副作用时再执行它。不过,这种方式应该是你的最后手段。
在可能的情况下,尽量只通过渲染来表达你的逻辑。你会惊讶于这能带你走多远!
Deep Dive
编写纯函数需要一些习惯和纪律。但它也能解锁一些令人惊叹的可能性:
- 你的组件可以在不同环境中运行——例如在服务器上!由于它们在相同输入下会返回相同结果,一个组件就可以服务于多个用户请求。
- 你可以通过跳过渲染未发生变化的组件来提升性能。这是安全的,因为纯函数总是返回相同结果,因此可以安全缓存。
- 如果某些数据在深层组件树渲染的中途发生变化,React 可以重新开始渲染,而不会浪费时间去完成已经过时的渲染。纯净性使得在任何时刻停止计算都变得安全。
我们正在构建的每一个新 React 特性都在利用纯净性。从数据获取到动画再到性能,保持组件纯净能释放 React 范式的力量。
Recap
- 组件必须是纯净的,这意味着:
- 各司其职。 它不应该改变渲染之前已经存在的任何对象或变量。
- 相同的输入,相同的输出。 在相同输入下,组件应始终返回相同的 JSX。
- 渲染可以在任何时候发生,所以组件不应该依赖彼此的渲染顺序。
- 你不应该修改组件用于渲染的任何输入。这包括 props、state 和 context。要更新屏幕,请“设置” state,而不是修改先前存在的对象。
- 尽量把组件的逻辑表达在你返回的 JSX 中。当你需要“改变某些东西”时,通常应该在事件处理函数中完成。作为最后手段,你可以使用
useEffect。 - 编写纯函数需要一些练习,但它能释放 React 范式的力量。
Challenge 1 of 3: 修复一个损坏的时钟
这个组件试图在午夜到早上六点之间把 <h1> 的 CSS 类设为 "night",并在其他时间设为 "day"。然而它并不起作用。你能修复这个组件吗?
你可以通过临时更改计算机的时区来验证你的解决方案是否有效。当当前时间在午夜到早上六点之间时,时钟应该显示反转的颜色!
export default function Clock({ time }) { const 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> ); }