6-2 根據天氣代碼顯示天氣圖示 - useMemo 的使用

本單元對應的專案分支為:prevent-redundant-computation-with-use-memo

單元核心#

這個單元的主要目標包含:

  • 根據天氣代碼顯示對應的天氣圖示
  • 了解 useMemo 的使用方式與使用時機

在上一個單元中,已經能夠根據天氣代碼轉換為天氣型態,最後再轉換為天氣圖示。在這個單元中要來實作天氣圖示的呈現,也就是讓天氣圖示能夠隨著天氣的不同而有變化,同時也會說明 React Hooks 中 useMemo 的使用時機與方式。

將天氣代碼的資料從 App 元件傳入 WeatherIcon#

App 元件#

由於天氣代碼的資料是在 App 元件中取得,WeatherIcon 並不知道,因此會透過 props 從 App 元件把資料傳入 WeatherIcon 元件,這裡把這個 props 的名稱取為 weatherCode

因為天氣圖示共分成白天和晚上兩種,這裡我們使用 moment 這個變數來判別是白天還是晚上。但目前因為從 API 只能取得「天氣代碼」,還沒有辦法得知當時是白天(day)或晚上(night),因此把 moment 先固定為晚上(night),以 props 的方式傳入元件中,而白天晚上的判斷會在後面的單元進行處理。

這裡會在 WeatherIcon 這個元件中透過 props 方式,把 weatherCodemoment 程式碼的變動如下:

// ./src/App.js
const App = () => {
// ...
const {
// ...
weatherCode, // 從 weatherElement 中取出 weatherCode 資料
} = weatherElement;
return (
<ThemeProvider theme={theme[currentTheme]}>
{/* ... */}
<CurrentWeather>
{/* 將 weatherCode 和 moment 以 props 傳入 WeatherIcon */}
<WeatherIcon weatherCode={weatherCode} moment="night" />
</CurrentWeather>
{/* ... */}
</ThemeProvider>
);
};

WeatherIcon 元件#

./src/components/WeatherIcon.js 的 WeatherIcon 元件中,已經可以透過 props 取得 App 元件傳進來的 weatherCodemoment

// ./src/components/WeatherIcon.js
// ...
const WeatherIcon = ({ weatherCode, moment }) => {
return /* ... */;
};

現在要呈現天氣圖示的方式很簡單,因為我們先前已經定義好了 weatherIcons 這個物件,透過這個物件,只要知道目前是白天或晚上(moment),接著知道天氣型態(weatherType),就可以得到對應的天氣圖示(weatherIcon)。所以可以像這樣寫:

// ./src/components/WeatherIcon.js
// ...
const weatherIcons = {
/* ... */
};
const weatherCode2Type = (weatherCode) => {
/* ... */
};
const WeatherIcon = ({ weatherCode, moment }) => {
const weatherType = weatherCode2Type(weatherCode); // 將天氣代碼轉成天氣型態
const weatherIcon = weatherIcons[moment][weatherType]; // 根據天氣型態和 moment 取得對應的圖示
return <IconContainer>{weatherIcon}</IconContainer>;
};

是不是相當簡潔呢?

這時候你應該可以看到畫面上已經出現對應的天氣圖示。

留意 weatherCode 和 moment 的值#

目前因為 moment 這個 props 在 App 元件中是先固定寫為 night ,所以 WeatherIcon 元件中的 moment 變數也會固定是 night,所以畫面上呈現的也會是晚上的天氣圖示(月亮),關於白天和晚上的判斷,會在後面的單元再繼續進行處理。

weatherCode 這個 props,會實際上透過 AJAX 的方式從中央氣象局的 API 取得。在這裡由 App 元件傳進來的 weatherCode 在還沒有從 API 拉取到資料前,會使用 useState 中所定義的預設值,也就是 0,並以 props 的方式傳入 WeatherIcon 元件中,也就是說一開始時,weatherCode 的值會是 0。

接著,要等到透過 AJAX 取得中央氣象局的資料回應後,App 元件會再把最新的 weatherCode 的值透過 props 再次傳入 WeatherIcon 元件中,這時候 WeatherIcon 元件才能取得最新的天氣代碼。

這裡你也可搭配先前安裝的 React DevTools,試著選到 WeatherIcon 元件後,去修改裡面 props 的 momentweatherIcon,看看天氣圖示會不會有不同的變化,例如,你也可以將 moment 的值從 night 改成 day,應該會看到天氣圖示從「夜晚的月亮」變成「白天的太陽」:

Imgur

props 更新時子層元件也會更新#

看到這裡讀者可能會有些好奇,為什麼這裡沒有使用 useState 提供的 setSomething 這種寫法,元件仍然會重新轉譯呢?

還記得最早的時候,我們直接改變元件內的變數時,元件的畫面並不會隨著變數改變而更新的情形嗎?那為什麼這裡 weatherCode 改變時,元件會重新更新呢?

這是因為現在 WeatherIcon 這個元件中,雖然沒有其內部的資料狀態(state),但有從父層 App 元件傳進來的 props,而 props 同樣算是在該元件內的資料狀態,只是它是從父層而來的。在 React 中,只要元件的資料狀態有改變,不論是該元件本身的 state 、或者是父層元件傳入的 props,都會觸發該元件重新轉譯

提示

React 可觀測到的資料狀態都可以透過 React Developer Tool 加以檢視,在這裡面你可以看到每個元件內所擁有的 state 和 props。

因此回到 WeatherIcon 元件中,雖然這裡沒有使用 useState,但因為父層 App 元件在透過 AJAX 取得 weatherCode 的值後,會再次透過 props 傳到 WeatherIcon 元件中,props 改變了,就觸發 WeatherIcon 元件重新轉譯。

以 useMemo 提升效能 - 避免不必要的重複運算#

在上面的程式碼中,做法並沒有什麼問題,但你會發現只要元件有重新轉譯的情況,例如,只要 weatherCode 或 moment 的資料有變動,或者未來這個元件中有其他 state,只要元件一更新,就需要透過 weatherCode2Type 這個方法重新取得 weatherCode。

如果是 weatherCode 改變而重新透過 weatherCode2Type 來取得最新的 weatherType 還算合理,但若是 moment 或其他 state 改變導致的畫面更新,明明 weatherCode 沒變,卻還要重新使用 weatherCode2Type 取得完全相同的結果,就顯得有些多餘。

這裡 weatherCode2Type 運算上不算太複雜,所以重複計算對效能的影響並不太大,但若未來函式需要做更複雜的運算,只要有資料狀態改變,即使變更的資料對計算結果沒有影響卻都需要重新計算的話(例如,變更的不是 weatherCode,卻又重新呼叫 weatherCode2Type) ,會使得效能變差,嚴重時 JavaScript 無法進行其他處理,也就是所謂的阻塞(blocking),將會使得使用者的體驗非常差。

為了解決這個問題,React 提供了 useMemo 這個 Hook 來幫助我們提升效能。

useMemo 的基本使用#

useMemo 這個 React Hook 和先前單元中提到的 useCallback 非常類似,主要目的都是效能優化的方法之一,兩者的差別主要在於 useCallback 回傳的是一個函式,而 useMemo 回傳的是一個計算好的值。

現在看一下 useMemo 的寫法:

const memoizedValue = useMemo(() => {
const result = computeExpensiveValue(a, b);
return result;
}, [a, b]);
  • 首先可以看到和多數的 React Hooks 一樣,它也有 dependencies array,只要 dependencies array 中的變數沒有任何改變,useMemo 中的函式內容就不會執行。
  • useMemo 的參數同樣是一個函式,在這個函式中可以進行一些複雜的運算,例如這裡的 computeExpensiveValue 或專案中 weatherCode2Type 這類的方法。
  • 在 useMemo 函式的最後,只需要把經過複雜運算的「值」回傳出來即可。

現在只要 dependencies array 的變數沒有改變,useMemo 內的函式就不會重複執行,而會直接取得上次運算得到的結果,如此來達到效能的提升。

將 weatherCode2Type 的運算結果保存下來#

回到 WeatherIcon 元件中,我們就可以透過 useMemo 這個 React Hook 來把 weatherType 保存下來,只要 weatherCode 沒有改變的情況下,就不需要透過 weatherCode2Type 重新運算取得 weatherType。

我們可以這麼做:

  1. 從 react 套件中取出 useMemo 方法
// ./src/components/WeatherIcon.js
import React, { useMemo } from 'react';
  1. 透過 useMemo 取得並保存 weatherCode2Type 計算的結果,回傳的結果一樣取名為 weatherType,你可以看到,只要把 weatherCode2Type 得到的運算結果作為回傳值即可。
// ./src/components/WeatherIcon.js
const WeatherIcon = ({ weatherCode, moment }) => {
// 使用 useMemo
const weatherType = useMemo(() => weatherCode2Type(weatherCode));
const weatherIcon = weatherIcons[moment][weatherType];
return <IconContainer>{weatherIcon}</IconContainer>;
};
  1. 最後不要忘了在 useMemo 的 dependencies,在陣列中放入 weatherCode,當 weatherCode 的值有變化的時候,useMemo 就會重新計算取值:
const WeatherIcon = ({ weatherCode, moment }) => {
// 記得要使用 useMemo 的 dependencies 陣列
const weatherType = useMemo(() => weatherCode2Type(weatherCode), [
weatherCode,
]);
const weatherIcon = weatherIcons[moment][weatherType];
return <IconContainer>{weatherIcon}</IconContainer>;
};

現在畫面上的天氣圖示一樣會根據天氣代碼加以改變,而且有了 useMemo,只有在 weatherCode 有改變的情況下,才會重新透過 weatherCode2Type 取得新的天氣型態。

useMemo 的目的是用來提升效能#

在這個基本的範例中,你可能沒辦法馬上感受到使用 useMemo 的作用,useMemouseCallback 都是作為效能優化的工具之一,這裡使用 useMemo 的目的是避免當元件重新轉譯但函式帶入的參數(weatherCode)相同時,所導致不必要的運算,因為 weatherCode 相同的情況下,回傳的 weatherType 一定也相同,根本不需要重新再算一次。

useCallback 一樣,多數的情況下沒有使用 useMemo 程式依然能夠正確運行,適當的使用這些優化效能的方法,可以幫助程式減少不必要的重複運算,但不適當或多餘的使用卻仍有可能導致效能變差。

換你了!練習看看 useMemo 的使用#

現在要請你把 weatherCodemoment 的資料透過 props 從 App 元件傳到 WeatherIcon 元件內,接著在 WeatherIcon 元件中搭配 weatherCode2Type 的方法,把天氣代碼轉成天氣型態,最後透過 weatherIcons 轉成要顯示的天氣圖示。

你可以參考下面的步驟:

  • 在 App 元件中將 weatherCodemoment 以 props 傳入 WeatherIcon 元件,moment 的值先固定為 night
  • 在 WeatherIcon 元件取得由 App 元件傳進來的 weatherCodemoment props
  • 將取得的 weatherCode 透過 weatherCode2Type 這個方法轉換為天氣型態 weatherType
  • 透過 weatherIconsweatherType 轉換為最終要顯示的天氣圖示(weatherIcon
  • 使用 useMemo 這個方法避免不必要的重複運算
  • 試著透過 React Developer Tools 修改 momentweatherCode 的值,檢視天氣圖示的改變

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 prevent-redundant-computation-with-use-memo 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/prevent-redundant-computation-with-use-memo

Imgur