什么是纯函式 (pure function)? 为什么 React 的函式元件需要是纯函式?
2022年2月4日
在 Hook 出现后,React 的函式元件 (Function Component) 逐渐成为主流。关于函式元件,最常见的问题之一,便是「为什么 React 的函式元件需要是纯函式 (pure function)?」。本篇将以第一人称的方式,拟答这个问题。
什么是纯函式 (pure function)?
纯函式是指带有以下两个主要特点的函式,第一是「只要有相同的输入,就会有相同的输出」;第二则是它不会改变函式以外的存在,换句话说不会有副作用(side effects)。
举例来说,f(x) = 2x
这个函式,只要输入的 x 是 1,输出的结果就会是 2;如果输入的 x 是 2,输出的就会是 4。不管呼叫多少次,输出都会是一样的,这就满足纯函式的第一个要件。相反地,假如函式当中有 Math.random()
这类随机性的运算,会让每次的输出不一定,就不能称为纯函式。
没有副作用,不改变函式以外的存在,则可以看到下面的例子 (推荐再回答这题时可以举例,会更好说明)。当本来在外的 guest
变数,因为 getCup
函式改变,我们就不能称 getCup
这个函式为纯函式。
let guest = 1;
function sayHi(message) {
guest = guest + 1;
return `${message} 跟 ${guest} guests `;
}
为什么 React 的函式元件需要是纯函式 (pure function)?
React 的函式元件之所以需要是纯函式,是因为当函式如果不纯 (impure),会让渲染出的画面不稳定,可能出现我们预期外的 UI 呈现。白话来说,就是比较容易造成 bugs。
如果每次的输入,会有不同的输出,那会让 UI 的呈现非常不稳定。以下面的代码为例,如果我们想要依据客人的人数,来准备巧克力派的食材,当有随机数出现,每次传入的客人数相同,可能会有不同的食材数量,这假如出现在食谱网站,那会是很不理想的 bug。
function ChocolatePie({ guestNum }) {
const milkNum = guestNum * Math.floor(Math.random() * 10)
const chocolateNum = guestNum * 2
return (
<div> 加入 {milkNum} 杯牛奶,以及 {chocolateNum} 条巧克力 </div>
);
}
// 如果实际渲染这三个元件,同样的 guestNum 会渲染出不同内容
<ChocolatePie guestNum={3} />
<ChocolatePie guestNum={3} />
<ChocolatePie guestNum={3} />
如果有副作用,也将会造成不稳定的 bugs。以下面来自 React 官方范例的代码来说,如果更动到了 Cup
元件外的 guest
,将会导致,即使渲染同一个 Cup
元件,却渲染出不同内容。这样的 bug 也是不理想的。
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
// 如果渲染下面这三个 Cup 元件,会有不同的结果
<Cup />
<Cup />
<Cup />
总结来说,当函式元件当中,有非纯函式的存在,就会不稳定有潜在的 bug;因此在写 React 的代码时,我们尽可能地写纯函式。
延伸题:如何在 React 侦测不纯的函式?
在 React 常见的函式,除了函式元件,也有 setState
、context
。在使用它们时,都要确保是纯函式。举例来说,如果作为输入传进去元件的 props
,如果传入同样的 props
却有不同的结果,就会造成不稳定的 bugs。同样地,当我们要改变某个状态,如果直接去改变值,而不是 setState
,会造成副作用,这也会容易产生 bugs。
要在 React 避免不小心犯了这类错误,可以很简单地使用严格模式 (Strict Mode)。当我们开启严格模式后,React 在渲染每个元件时,都渲染两次;只要是不纯的函式,就很可能在两次渲染出现不同结果,这可以让我们更侦测出是哪里出问题。