有些 JavaScript 的函式為「純函式」。純函數只會執行計算,不會做別的事情。如果我們嚴格地把 component 都寫成純函數,就可以在隨著 codebase 的增長中避免一系列令人困惑且不可預期的問題出現。但是在獲得這些好處前,你必須先遵守一些規則。
You will learn
- 什麼是存粹性以及它如何幫助你避免錯誤
- 如何透過將更改保留在渲染階段外,來保持 Component 的 pure
- 如何使用嚴格模式來尋找 Component 中的錯誤
純粹性:Component 像是公式
在計算機科學中(尤其是函數程式設計),「純函數」具有以下的特徵:
- 不多管閒事 這個函數不會修改任何在他被呼叫之前就已經存在的物件或變數。
- 一樣的輸入,一樣的輸出 只要我們輸入相同的參數,這個函數總是回傳一個相同的輸出。
你可能已經熟悉純函數的其中一個例子:數學中的公式
來看這個數學公式: y = 2x
如果 x = 2 那麼 y = 4 ,永遠如此。
如果 x = 3 那麼 y = 6,永遠如此。
如果 x = 3 , y 不會因為一天中的時間或是股票市場的狀態而有時候是 9 或 –1 或 2.5。
如果 y = 2x 且 x = 3, y 永遠都會是 6。
如果我們把它放到 JavaScript 函數中,它會長得像這樣:
function double(number) {
return 2 * number;
}
在上面的範例中, double
是一個純函數。如果你傳入 3
,他永遠都會回傳 6
。
React 就是圍繞這個概念設計的。 React 假設你編寫的每個函數都是純函數。 這表示你編寫的所有 React Component 都必須永遠在給定相同輸入的情況下,回傳相同的 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 cups of water
的 JSX。
當你傳入 drinkers={4}
,會永遠回傳包含 4 cups of water
的 JSX。
就像是數學公式一樣。
你可以把 Component 想成是食譜一樣: 如果你遵循它們並且在烹飪過程中不加入新食材,那麼你每次都會得到相同的菜餚。這個「菜餚」就是 Component 提供給 React Render 的 JSX。
Illustrated by Rachel Lee Nabors
副作用:(非)預期的後果
React 的渲染過程必須永遠保持 pure 。 Components 應該永遠 回傳 它們的 JSX,而不該 更改 任何渲染之前就存在的物件或變數 - 這會使它們變得 impure !
這是一個違反規則的 Component :
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 /> </> ); }
這個 component 正在讀取與更改在外部宣告的 guest
變數。這意味著多次呼叫這個 component 會產生不一樣的 JSX ! 更重要的是,如果 其他 components 也讀取 guest
,它們將依照被渲染的時間點而產生不一樣的 JSX !這是不可預測的。
回到我們的公式 y = 2x,即使現在 x = 2, 我們不能保證 y = 4。我們的測試可能會失敗、我們的用戶可能會感到困惑、飛機會從天上掉下來 - 你可以看到這將會導致令人困惑的錯誤!
你可以透過 將 guest
當成 prop 傳入 來修正這個 component:
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} /> </> ); }
現在你的 component 是 pure 的,因為它返回的 JSX 僅依賴 guest
prop。
一般來說,你不應該預期 components 以任何特定順序渲染。在 y = 2x 之前或之後調用 y = 5x 並不重要:兩個公式都將各自獨立地求解。同樣的,每個 component 都應該「只考慮自己」, 而不是在渲染過程中試圖與其他 components 協調或是依賴其他 components 。渲染就像是一個學校考試:每個 components 都應該計算自己的 JSX !
Deep Dive
僅管你可能還沒有全部使用過它們,但在 React 中你可以在渲染時讀取三種輸入:props、state 以及 context。你應該永遠將這些輸入視為 read-only 。
當你想要 改變 某些以用戶輸入為響應的內容時,你應該要 set state 而非直接更改變數。你永遠都不該在 component 渲染過程中改變已存在的變數或物件。
React 提供了「嚴格模式」,在開發過程中它會呼叫每個 component 的函數兩次。 透過呼叫兩次 component 的函數,嚴格模式有助於找到違反這些規則的 components。
請注意在原本的範例,它顯示了「Guest #2」、 「Guest #4」以及「Guest #6」,而不是「Guest #1」、「Guest #2」及「Guest #3」。原本的函數是 impure 的,所以呼叫兩次後就破壞了它。但在修正後的 pure 版本中,即使每次呼叫了兩次函數還是能夠正常運作。 純函數只進行運算,因此呼叫兩次後也不會改變任何事 — 就像是呼叫 double(2)
兩次也不會改變它的回傳值,求解 y = 2x 兩次不會改變 y 的值。相同的輸入永遠會有相同的輸出。
嚴格模式不會影響正式環境,因此它不會拖慢用戶的應用程式速度。如需選擇嚴格模式,你可以將你的 root component 包裝到 <React.StrictMode>
。有些框架預設會這麼做。
變異本地化:你的 component 的小秘密
在上面的範例中, 問題是 component 在渲染時改變了 預先存在的 變數。這通常會稱之為 變異 (mutation) 使其聽起來有點可怕。純函數不會改變函數範圍外的變數、或是調用之前就已建立的物件 — 這使得它們 impure!
不過, 在渲染時改變 剛剛 才建立的變數或物件是完全沒問題的。在這個範例中,你建立了 []
陣列,並賦值給 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
範圍外的任何程式碼都不會知道發生了這個情況。這稱為 “local mutation”- 這就像是 component 自己的小秘密。
可能 會引起副作用的地方
雖然函數程式設計在很大程度上依賴於存粹性,但在某些時候,有些東西 必須改變。這就是程式設計的意義所在!這些更改例如:顯示畫面、起始一個動畫、更改資料都被稱為 副作用 。他們是 一邊發生 的事情,而不是在渲染期間發生的事情。
在 React 中,副作用通常屬於事件處理器。事件處理器是 React 在你執行某些操作(例如:點擊一個按鈕)時運行的函數。儘管事件處理器是在 component 內部 定義的,但它們 不會在渲染時間執行!所以事件處理器不需是 pure 的。
如果你已經用盡了所有其他選項,並且無法找到其他適合你的副作用的事件處理器,你仍然可以選擇 component 中的 useEffect
來將其附加到返回的 JSX。這告訴 React 在渲染後、允許副作用的情況下執行它。但是,這個方法應該要是你最後的手段。
可以的話,盡量嘗試透過渲染過程來表示你的邏輯。你會驚訝它能帶你走多遠!
Deep Dive
撰寫純函數需要一些習慣與紀律。但純函數也解鎖了一些絕佳的功能:
- 你的 components 可以在不同環境上運行 - 例如,在伺服器上!由於它們對相同輸出會有相同結果,因此一個 component 可以滿足許多用戶請求。
- 你可以透過 跳過渲染 那些 input 沒有更新的 components 來提昇性能。這是安全的,因為純函數永遠都會回傳相同的結果,所以可以安全地快取它們。
- 如果在渲染深層元件樹 (deep component tree) 的過程中某些資料發生變化,React 可以重新啟動渲染、而不浪費時間完成過時的渲染。純粹性可以讓它更安全地隨時停止計算。
所有我們正在建立的 React 新功能都利用了純粹性的優點。從獲取資料到動畫再到性能,保持 components 的存粹性能夠解鎖 React 典範的力量。
Recap
- 一個 component 是 pure 的,這意味著:
- 不多管閒事 這個函數不會修改任何在它被呼叫之前就已經存在的物件或變數。
- 一樣的輸入,一樣的輸出 只要我們輸入相同的參數,這個函數總是回傳一個相同的輸出。
- 渲染可能會在任何時間發生,因此 components 不該依賴於彼此的渲染順序。
- 你不該改變任何你的 components 用來渲染的輸入。這包含 props,state,以及 context。要更新畫面的話,請 “set” state 而不是直接修改預先存在的物件。
- 盡量在返回的 JSX 中表達你的 component 邏輯。當你需要「更改內容」時,你會希望在事件處理器中處理。或是作為最後的手段,你可以使用
useEffect
。 - 撰寫純函數需要一些練習,不過它能解鎖 React 典範的力量。
Challenge 1 of 3: 修正一個換掉的時鐘
這個 component 希望在午夜至上午 6 點期間將 <h1>
的 CSS class 設置為 "night"
,並在所有其他時段都設成 "day"
。但是它沒辦法運作。你能修復這個 component 嗎?
你可以透過暫時修改電腦的時區來驗證你的做法是否有效。當時間在午夜至上午 6 點時,時鐘的顏色應該要是反白的!
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> ); }