保持元件的純粹性

有些 JavaScript 函式是*純粹的*。純粹函式只執行計算,不會做其他事情。藉由嚴格地將元件撰寫為純粹函式,可以避免隨著程式碼庫增長而產生一整類令人困惑的錯誤和不可預測的行為。 然而,要獲得這些好處,必須遵循一些規則。

你將學到

  • 什麼是純粹性,以及它如何幫助你避免錯誤
  • 如何透過將變更排除在渲染階段之外來保持元件的純粹性
  • 如何使用嚴格模式來查找元件中的錯誤

純粹性:元件作為公式

在電腦科學中(尤其是在函數式程式設計的世界中),純粹函式是指具有以下特性的函式:

  • 它只管自己的事。 它不會更改在呼叫它之前就存在的任何物件或變數。
  • 相同的輸入,相同的輸出。 給定相同的輸入,純粹函式應始終返回相同的結果。

你可能已經熟悉純粹函式的一個例子:數學公式。

考慮這個數學公式: y = 2x

如果 x = 2,則 y = 4。永遠都是。

如果 x = 3,則 y = 6。永遠都是。

如果 x = 3y 不會根據一天中的時間或股市的狀態有時是 9–12.5

如果 y = 2xx = 3y 將*永遠*是 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>
  );
}

當你將 drinkers={2} 傳遞給 Recipe 時,它將返回包含 2 杯水 的 JSX。永遠都是。

如果你傳遞 drinkers={4},它將返回包含 4 杯水 的 JSX。永遠都是。

就像數學公式一樣。

你可以將你的元件想像成食譜:如果你按照它們操作,並且在烹飪過程中不引入新的成分,你每次都會得到相同的菜餚。那個「菜餚」就是元件提供給 React 進行 渲染 的 JSX。

A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk

插圖作者 Rachel Lee Nabors

副作用:(非)預期後果

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 />
    </>
  );
}

這個元件正在讀取和寫入一個在它外部宣告的 guest 變數。這表示 多次呼叫這個元件將產生不同的 JSX! 更重要的是,如果*其他*元件讀取 guest,它們也會根據渲染時間產生不同的 JSX!這是不可預測的。

回到我們的公式 y = 2x,現在即使 x = 2,我們也不能相信 y = 4。我們的測試可能會失敗,我們的用戶會感到困惑,飛機可能會從天上掉下來——你可以看到這會導致多麼令人困惑的錯誤!

您可以通過 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 = 5x 之前還是之後調用 y = 2x 都沒有關係:兩個公式都會彼此獨立地解析。同樣地,每個組件應該只「為自己思考」,而不是在渲染過程中嘗試與其他組件協調或依賴其他組件。渲染就像學校考試:每個組件都應該自己計算 JSX!

深入探討

使用 StrictMode 偵測不純的計算

雖然您可能尚未全部使用它們,但在 React 中,您可以在渲染時讀取三種輸入:propsstatecontext。您應該始終將這些輸入視為唯讀。

當您想要響應使用者輸入來*更改*某些內容時,您應該 設定 state 而不是寫入變數。在組件渲染時,您永遠不應該更改預先存在的變數或物件。

React 提供了一種「嚴格模式 (Strict Mode)」,在開發過程中,它會呼叫每個組件的函式兩次。透過呼叫組件函式兩次,嚴格模式有助於找出違反這些規則的組件。

請注意原始範例如何顯示「訪客 #2」、「訪客 #4」和「訪客 #6」,而不是「訪客 #1」、「訪客 #2」和「訪客 #3」。原始函式是不純的,因此呼叫它兩次會破壞它。但是,即使每次呼叫函式兩次,修復後的純粹版本仍然有效。純函式只進行計算,因此呼叫它們兩次不會改變任何東西——就像呼叫 double(2) 兩次不會改變回傳值一樣,解 y = 2x 兩次也不會改變 y 的值。相同的輸入,相同的輸出。永遠如此。

嚴格模式在生產環境中沒有作用,因此它不會降低應用程式的速度。要選擇使用嚴格模式,您可以將您的根組件包在 <React.StrictMode> 中。某些框架預設會執行此操作。

局部突變:您組件的小秘密

在上面的例子中,問題是組件在渲染時更改了一個*預先存在*的變數。這通常被稱為 「突變」,讓它聽起來更可怕一些。純函式不會突變函式作用域之外的變數或在呼叫之前建立的物件——這會使它們不純!

但是,在渲染時更改您*剛剛*建立的變數和物件是完全沒問題的。 在這個例子中,您建立了一個 [] 陣列,將其賦值給 cups 變數,然後將一打杯子 push 進去。

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 變數或 [] 陣列是在 TeaGathering 函式之外建立的,那將是一個巨大的問題!您將透過將項目推送到該陣列來更改*預先存在*的物件。

但是,這沒問題,因為您是在 TeaGathering 內部*在同一次渲染期間*建立它們的。TeaGathering 之外的任何程式碼都不會知道這件事發生了。這稱為 「局部突變」——就像您組件的小秘密一樣。

您*可以*造成副作用的地方

雖然函數式程式設計 heavily 依賴純粹性,但在某個時候,某個地方,*某些東西*必須改變。這就是程式設計的重點!這些更改——更新螢幕、開始動畫、更改資料——稱為 副作用。它們是*「在旁邊」*發生的,而不是在渲染過程中發生的。

在 React 中,副作用通常屬於 事件處理程式 內部。 事件處理程式是當您執行某些動作時 React 執行的函式——例如,當您點擊按鈕時。即使事件處理程式是在組件*內部*定義的,它們也不會在渲染*期間*運行!因此事件處理程式不需要是純粹的。

如果您已嘗試所有其他選項,但仍找不到適合您副作用的事件處理程式,您仍然可以透過在組件中使用 useEffect 呼叫將其附加到您回傳的 JSX。這會告訴 React 在渲染之後,允許副作用時執行它。但是,這種方法應該是您的最後手段。

如果可能,請嘗試僅使用渲染來表達您的邏輯。您會驚訝於這可以帶您走多遠!

深入探討

為什麼 React 重視純粹性?

撰寫純函式需要一些習慣和紀律,但它也開啟了絕佳的機會。

  • 您的組件可以在不同的環境中運行,例如在伺服器上!由於它們對於相同的輸入返回相同的結果,因此一個組件可以服務許多用戶端請求。
  • 您可以透過跳過渲染輸入沒有變化的組件來提高效能。這是安全的,因為純函式始終返回相同的結果,因此可以安全地快取。
  • 如果在渲染深度組件樹的過程中某些數據發生變化,React 可以重新開始渲染,而不會浪費時間完成過時的渲染。純粹性使得隨時停止計算都是安全的。

我們正在構建的每個新的 React 功能都利用了純粹性。從數據提取到動畫再到效能,保持組件純粹可以釋放 React 範例的威力。

重點回顧

  • 組件必須是純粹的,這意味著
    • 它只管自己的事。它不應該更改渲染之前存在的任何物件或變數。
    • 相同的輸入,相同的輸出。給定相同的輸入,組件應始終返回相同的 JSX。
  • 渲染可以隨時發生,因此組件不應依賴彼此的渲染順序。
  • 您不應改變組件用於渲染的任何輸入。這包括 props、狀態和上下文。要更新畫面,請“設定”狀態而不是改變現有的物件。
  • 盡量在您返回的 JSX 中表達組件的邏輯。當您需要“更改事物”時,您通常會希望在事件處理程序中執行此操作。作為最後手段,您可以使用 useEffect
  • 撰寫純函式需要一些練習,但它可以釋放 React 範例的威力。

挑戰 1 3:
修復損壞的時鐘 ..<

此組件嘗試在午夜到早上六點之間將 <h1> 的 CSS 類別設定為 "night",而在其他所有時間設定為 "day"。但是,它不起作用。您可以修復此組件嗎?

您可以透過暫時更改電腦的時區來驗證您的解決方案是否有效。當目前時間介於午夜到早上六點之間時,時鐘應該具有反轉的顏色!

export default function Clock({ time }) {
  let 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>
  );
}