2-11 事件處理器的重構

在這個單元中我們會把計數器做簡單的整理。一般在開發程式的過程中,有時候會先專注於功能的實作,等到功能實作出來,可以正常運作後,會再把整個程式碼做整理,這個動作一般我們稱作重構(refactoring)。

重構程式碼最主要的目的是幫助未來的自己或其他開發者來減少後續維護上的困難,過程中可能會刪除不必要的變數、重新為變數命名以增加易讀性、減少同樣邏輯但內容重複的程式碼等等。

在這個單元中我們會針對計數器中的點擊事件進行重構,讓我們來看一下可以怎麼做。

將事件處理的邏輯從 JSX 中抽離#

在先前的程式碼中,我們是把使用者點擊按鈕時要做的事直接放在 onClick={}{} 內去執行,像是這樣:

<div className="chevron chevron-up" onClick={() => setCount(count + 1)} />

這裡因為 onClick 後只需呼叫 setCount 這個方法,因此並不會有什麼大問題,但若現在 onClick 後需要做更多的事情時,直接把這個事件處理器(event handlers)寫在 JSX 的行內可能就會變得比較難管理。因此,為了程式碼管理上的方便,可以把事件處理器先定義成一個函式,在 onClick 後去呼叫這個函式即可,如此可以達到畫面和邏輯的分離。

這裡我們把 onClick 裡面的函式拉出來,分別取名為 handleIncrementhandleDecrement,像是這樣:

const Counter = () => {
const [count, setCount] = useState(5);
// 將事件處理器獨立成 handleIncrement 和 handleDecrement
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
<div className="container">
<div
// 把 handleIncrement 在這裡帶入
onClick={handleIncrement}
className="chevron chevron-up"
style={{
visibility: count >= 10 && 'hidden',
}}
/>
<div className="number">{count}</div>
<div
// 把 handleDecrement 在這裡帶入
onClick={handleDecrement}
className="chevron chevron-down"
style={{
visibility: count <= 0 && 'hidden',
}}
/>
</div>
);
};

換你了:把事件處理的程式邏輯從 JSX 中抽離吧#

現在請你試著照上面的方式,把事件處理的程式邏輯從 JSX 中抽離吧!你可以透過下方連結或是 Github 專案說明頁的「從 JSX 中拆分事件處理器的邏輯」來檢視完整程式碼:

https://codepen.io/PJCHENder/pen/gOPMzmz

Imgur

同樣邏輯的程式碼不必重複#

雖然現在程式碼看起來又乾淨了不少,但你可能會想說,handleIncrementhandleDecrement 做的事好像差不多,那可不可以把它們包在一起,寫成一個稱作 handleClick 的函式,接著 handleClick 中帶入一個名為 type 的參數,當 typeincrement 的時候就呼叫 setCount(count + 1);當 typedecrement 的時候就呼叫 setCount(count - 1)

於是我們可以把程式碼再進一步改成:

const Counter = () => {
const [count, setCount] = useState(5);
// 桶整成一個名為 handleClick 的方法
const handleClick = (type) => {
if (type === 'increment') {
setCount(count + 1);
}
if (type === 'decrement') {
setCount(count - 1);
}
};
return <div className="container">{/* ... */}</div>;
};

留意帶有參數的事件處理器#

但這裡有一個 JavaScript 的概念要特別留意,現在 handleClick 本身是一個函式,如果我們在 JSX 中直接把 type 當成參數帶進去函式,像這樣的話:

// 這是錯誤的寫法,請不要照做
// 向上點擊的箭頭
<div
onClick={handleIncrement('increment')}
className="chevron chevron-up"
style={{
visibility: count >= 10 && 'hidden',
}}
/>

當你照著這麼做的時候,實際上會發生無窮迴圈的情況,並且顯示錯誤訊息:

Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.

這裡我們需要特別留意 onClick 後面的內容是 handleIncrement('increment'),這種寫法和剛剛我們寫 onClick={handleIncrement} 不同,我們預期的的是「當使用者點擊按鈕時,會去執行 handleClick('increment') 這個方法」。但實際上,因為 handleClick 後面直接加上了小括號 ('increment'),因此當 JavaScript 執行到這裡的時候,這個 handleClick 函式就已經被執行了

imgur

所以實際上畫面在轉譯(render)的時候,就執行了 handleClick 這個函式,這時候就呼叫到了 setCount();當 setCount 被呼叫到時,React 發現就會去檢查 count 的值,發現 count 不一樣之後,又會去更新畫面,於是就進入了無限迴圈...:

imgur

這也就是為什麼在錯誤訊息中會看到「Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.」,因為它陷入無窮迴圈,畫面一直重複轉譯。

要解決這個問題只需要把 handleClick() 包在一個函式中,讓它不會在畫面轉譯時馬上被執行,寫法上可以這麼做:

<div
onClick={() => handleClick('increment')}
className="chevron chevron-up"
style={{
visibility: count >= 10 && 'hidden',
}}
/>

這樣的話,畫面轉譯的時候 handleClick 就不會馬上被執行,而是在使用者點擊按鈕的時候才會去執行 () => handleClick('increment') 這個函式。

使用三元判斷式(ternary operator)簡化語法#

在先前的單元中我們曾提到邏輯運算子,也就是 ||&& 的使用。除了邏輯運算子之外,三元判斷式在 React 中也非常常用,三元判斷式的語法中會使用到 ?: ,在 ? 前面放的會是一個判斷式,當這個判斷式的條件為 true 時,會執行介於 ? 後面和 : 前面的內容;但當這個判斷式的條件為 false 時,則會執行 : 後面的內容。

以下面的例子來看:

// (判斷式) ? (條件為真) : (條件為假)
const averageHeight = 170;
const tall = 180 > averageHeight ? 'I am tall.' : 'I am short.'; // I am tall
const short = 160 > averageHeight ? 'I am tall.' : 'I am short.'; // I am short
  • 第一個變數 tall 的值,因為 ? 前面的判斷式是(180 > averageHeight)是 true,所以會執行介於 ?: 之間的內容,並得到 I am tall 的結果。
  • 第二個變數 short 的值,因為 ? 前面的判斷式會是 false,所以會執行 : 後面的內容,因而得到 I am short 的結果。

回到我們的 handleClick 的方法,原本的 if 一樣可以改成用這種三元判斷式來表示,像是這樣:

// 使用三元判斷式簡化語法
const handleClick = (type) => {
setCount(type === 'increment' ? count + 1 : count - 1);
};

這裡又由於在這個箭頭函式中並不需要做其他處理,而單純是呼叫一個函式,因此又可以把箭頭後面的大括號 {} 省略,簡化成:

const handleClick = (type) =>
setCount(type === 'increment' ? count + 1 : count - 1);

到這裡我們就完成了事件處理器的重構!

換你了:簡化事件處理器的語法#

這些簡化的語法一開始看會覺得暈頭轉向的,好像同樣的功能卻用不同的寫法換來換去的,這麼做的目的通常是讓程式碼變得更精簡,但有些時候也不能一昧的精簡程式碼而忽略了程式本身的易讀性,這個部分多會需要更多的實作經驗才會比較清楚哪些精簡的方式是其他開發者容易理解的,哪些會造成其他開發者閱讀上的困惑。

現在,請你試著練習看看,把事件處理器的語法加以簡化,若實作上有碰到問題,都可以回頭來看下方連結的程式碼,或於 Github 專案說明頁檢視連結:

https://codepen.io/PJCHENder/pen/qBbNYVr

Imgur

進階寫法:讓函式執行後回傳另一個函式(選讀)#

再來這個是更為進階的寫法,如果上面的內容閱讀起來你覺得還算輕鬆,那麼你可以繼續閱讀這個部分;如果上面的內容已經讓你感到有些暈頭轉向的話,那就先略過這個部分沒有關係,並不會影響到後面 React 的學習。

在上面的程式碼中,你會發現我們雖然把事件處理器抽成了獨立的 handleClick 函式,但因為我們需要在這個函式中帶入參數,為了避免函式直接在 JSX 中被執行,變成在 onClick 的時候又要多包一層函式,像是這樣:

<div onClick={() => handleClick('increment')} />

但其實如果對 JavaScript 夠熟練的話,還可以用其他的寫法,也就是在 JSX 執行的時候我們就讓 handleClick 直接被執行,同時讓它被執行的時候就把 type 這個參數的值給帶入;但在 handleClick 執行後會回傳另一個函式,這個回傳的函式才是真正在使用者點擊時會被執行到的。整個語法會像這樣:

// 讓 handleClick 被執行時,實際上是回傳一個函式
const handleClick = (type) => {
return () => {
setCount(type === 'increment' ? count + 1 : count - 1);
};
};

這時候就可以放心地讓 handleClick 在 JSX 被執行時直接被呼叫:

<div onClick={handleClick('increment')} />

之所以可以這樣寫,是因為當畫面轉譯的時候,雖然 handleClick('increment') 會馬上被執行沒錯,但 handleClick('increment') 執行後並不是馬上去呼叫 setCount 方法,而是在 handleClick 這個函式執行時,會把 increment 帶入函式內,接著回傳了另一個函式到 onClick={}{} 內。這個被回傳的函式一樣會在按鈕的點擊事件被觸發時被呼叫到:

// 當 JSX 執行後,onClick 中的內容會變成 handleClick('increment') 執行後會傳的函式,也就是
<div
onClick={() => {
setCount(type === 'increment' ? count + 1 : count - 1);
}}
/>

如果這裡的概念還能理解的話,最後 handleClick 這個函式本身又可以做箭頭函式上語法的簡化:

// 原本的寫法
const handleClick = (type) => {
return () => {
setCount(type === 'increment' ? count + 1 : count - 1);
};
};

先針對 handleClick 「裡面」的箭頭函式,可以省略箭頭後面的 {}

const handleClick = (type) => {
return () => setCount(type === 'increment' ? count + 1 : count - 1);
};

接著,針對 handleClick 箭頭函式「本身」,也可以省略箭頭後面的 {},最終會變成:

// () => () => {...}
const handleCLick = (type) => () =>
setCount(type === 'increment' ? count + 1 : count - 1);

因此,未來如果你看到像是 () => () => {...} 這樣的語法的話,不用太過驚訝,這不是什麼新的語法,單純只是在呼叫了一個函式之後會回傳另一個函式的簡化寫法。

換你了:讓一個函式回傳另一個函式#

若想要使用這種讓一個函式回傳另一個函式的寫法,需要先對 JavaScript 有一定的熟悉度才不會讓自己頭昏眼花,因此如果現階段還不太能理解這種寫法的話也不用擔心,先跳過去是可以的,並不會影響到後面內容的閱讀和 React 的學習。

這個部分的程式碼可以參考下面的連結,或於 Github 說明頁檢視連結「進階寫法:讓函式執行後回傳另一個函式」:

https://codepen.io/PJCHENder/pen/OJMXZGq

Imgur