有些JavaScript函数是纯函数。纯函数只执行计算。通过严格地只将组件编写为纯函数,可以避免随着代码库的增长而出现令人困惑的bug和不可预测的行为。然而,为了获得这些好处,你必须遵守一些规则。
纯函数
在计算机科学(尤其是函数式编程领域)中,纯函数是具有以下特征的函数:
- 它处理与自己有关的任务。它不会改变函数之外的任何对象或变量。
- 给定同样的输入,输出总是一样的。给定相同的输入,纯函数总是返回相同的结果。
纯函数的一个例子:数学公式。
看这个数学公式:y = 2x。
如果x = 2,那么总是y = 4。
如果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>
);
}
将drinks ={2}传递给Recipe时,总是返回包含2杯水的JSX。
将drinks ={4}传递给Recipe时,总是返回包含4杯水的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!这是不可预测的。
通过把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只依赖prop guest。
组件能以任何顺序呈现,在y = 5x之前或之后调用y = 2x并不重要,两个公式都是相互独立的。每个组件应该只“为自己考虑”,在渲染过程中不与其他组件协作或依赖,每个组件都应该自己计算JSX !
局部变量
在渲染时更改刚刚创建的变量和对象完全没有问题。在这个例子中,你创建了一个[]数组,将它赋值给一个cups变量,然后调用push()方法向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;
}
如果cups变量或[]数组是在teaggathering函数之外创建的,将会有一个很大的问题!
在渲染函数teaggathering中创建的cups变量。函数teaggathering之外的代码不知道发这个cups变量。这个cups局部变量不会产生副作用。
在哪里能使用副作用
虽然函数式编程在很大程度上依赖于纯粹性,但在某些时候,某些地方必须做出改变。这就是编程的意义所在!这些变化(更新屏幕、开始动画、改变数据)被称为副作用。它们是“在旁边”发生的事情,而不是在渲染过程中。
在React中,副作用通常在事件处理函数中。事件处理函数是在执行某些操作(例如单击按钮)时React运行的函数。即使在组件内部定义了事件处理程序,它们也不会在渲染期间运行!所以事件处理函数不需要是纯的。
如果你想了很多方法也不能找到副作用存放的地方,你可以尝试放到组件的useEffect中。渲染完以后,就能执行useEffect里面的副作用。然而,这种方法应该是你最后的选择。
参考文献: Keeping Components Pure