什麼是純函式 (pure function)? 為什麼 React 的函式元件需要是純函式?

2022年2月4日

💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容

在 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 常見的函式,除了函式元件,也有 setStatecontext 。在使用它們時,都要確保是純函式。舉例來說,如果作為輸入傳進去元件的 props,如果傳入同樣的 props 卻有不同的結果,就會造成不穩定的 bugs。同樣地,當我們要改變某個狀態,如果直接去改變值,而不是 setState ,會造成副作用,這也會容易產生 bugs。

要在 React 避免不小心犯了這類錯誤,可以很簡單地使用嚴格模式 (Strict Mode)。當我們開啟嚴格模式後,React 在渲染每個元件時,都渲染兩次;只要是不純的函式,就很可能在兩次渲染出現不同結果,這可以讓我們更偵測出是哪裡出問題。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們