保持组件纯粹:纯函数原则与副作用隔离
在数学中,公式 具有一种非常优美的特质:无论你在昨天计算、今天计算、在地球计算还是在月球计算,只要 , 的答案永远且唯一地等于 。它绝对不会因为今天天气不好,就突然算出 。
这种只负责根据输入计算输出、绝不产生节外生枝副作用的函数,在编程中被称为 纯函数(Pure Function)。
React 的整个架构都是建立在这个概念之上的。React 假定你编写的每一个组件都是纯函数!
1. 什么是纯函数?
一个函数只要满足以下两点,它就是纯函数:
- 只管自己的业务(Minds its own business):在被调用之前就已存在的任何对象或变量,它都不会去修改;
- 相同的输入,相同的输出(Same inputs, same outputs):只要传入的参数相同,无论被调用多少次,返回的 JSX 必须一模一样。
2. 反例演示:违反纯函数原则的杯子计数器
看看下面这个组件,它试图计算来访的客人杯数:
// ❌ 极度危险的不纯组件!
let guest = 0; // 在组件外部定义了一个全局变量
function Cup() {
// 缺陷:在渲染计算过程中直接修改了外部预先存在的变量!
guest = guest + 1;
return <h2>为第 {guest} 位客人准备的茶杯</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
发生了什么问题?
这个组件在渲染自己的时候,偷偷修改了外部变量 guest。
如果你渲染三次 <Cup />,它可能显示 1、2、3。但如果页面其他地方更新,导致 TeaSet 重新渲染,guest 就会从 3 变成 4、5、6!每次重新渲染,界面上的数字就全乱了套。
纯洁的改写方案:通过 Props 传参
让组件符合数学公式的纯洁性:需要什么数据,就从参数里光明正大地传进来!
// ✅ 纯函数组件
function Cup({ guest }) {
return <h2>为第 {guest} 位客人准备的茶杯</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
现在,只要传入 guest={1},它就永远返回“为第 1 位客人准备的茶杯”,没有任何意外。
3. 为什么 React 在开发模式下会渲染两次?(StrictMode 的秘密)
在学习 React 时,你可能会发现一个奇怪的现象:如果你在组件函数里写一个 console.log('渲染了'),控制台总是会连续打印两次!
这是因为 React 在开发环境下默认开启了 严格模式(StrictMode)。
为什么要执行两次?
在生产环境中,React 只会渲染一次。但在开发环境中,React 故意把你的每个组件函数连续调用两次! 其目的非常简单直接:帮你揪出像上面那种“不纯”的组件!
- 如果你的组件是纯函数,调用两次得到的结果完全一致,界面丝毫不受影响;
- 如果你的组件不纯(比如修改了外部变量),调用两次就会立即露馅(比如杯子数字突然跳成了 2、4、6),让你在上线前立即发现并修复隐藏的致命 bug。
4. 什么是合法的“局部突变”(Local Mutation)?
纯函数规则要求:不能修改在调用前就已存在的变量。 但是,在组件函数内部刚刚新建的变量,是完全可以随意修改的! 这被称为 局部突变(Local Mutation)。
看下面这个例子:
function Cup({ guest }) {
return <h2>为第 {guest} 位客人准备的茶杯</h2>;
}
export default function TeaGathering() {
const cups = []; // 在函数内部新建的局部数组
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />); // 尽管调用了 push,但这是完全纯粹的!
}
return cups;
}
因为 cups 是在本次 TeaGathering 执行过程中才从无到有创建的,外界任何其他组件都看不到它。在函数算完并返回之前,你可以在内部用 for 循环或 push 填充它。这完全符合纯函数的定义!
5. 那么,副作用(Side Effects)到底应该发生在哪里?
页面开发总归是需要改变东西的:向服务器发送请求、弹出一个提示框、跳转路由、保存数据。这些改变外部系统的行为都属于 副作用(Side Effects)。
React 并不反对副作用,但要求:副作用不能发生在组件的渲染计算过程中!
在 React 中,副作用主要居住在两个专门的庇护所中:
- 事件处理函数(Event Handlers):比如用户点击按钮时触发的
handleClick函数。它们在组件渲染完成后等待用户操作才执行,不属于纯计算渲染代码; - useEffect Hook:当某些副作用确实必须由页面加载自动引发(例如拉取远程数据),React 提供了专门的
useEffect机制,在渲染完成提交到 DOM 之后再去执行(我们会在本课程的最后一节深入学习)。
总结
- React 组件必须是纯函数:给定相同输入,必须返回相同 JSX;
- 绝不在渲染计算中修改外部已经存在的变量或对象;
- 在组件内部创建并修改的临时变量是合法的局部突变;
- StrictMode 在开发模式下调用组件两次,目的是帮你尽早发现不纯组件的隐蔽 bug;
- 更改系统和提交请求等副作用,应当放在事件处理程序或 Effect 中。