iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 4
0
Modern Web

關於React,那些我不知道的系列 第 4

關於props的記憶,React Memo (新增範例及說明)

Demo1 Codesandbox
Demo2 Codesandbox
我們知道影響 React 重新渲染的兩大關鍵 props / state ,當virtual-DOM發現props或state改變時,就會渲染使用這些數據對應的畫面UI。因此在優化效能的路上,就是需要減少一些不必要但卻很昂貴的渲染。

昨天我們談到了useMemo,可以在某個對應的state沒變時,不重新渲染某個數值及其畫面。那props也有個對應的工具存在,也就是說當props沒改變時,我們就不去重新渲染畫面。這個東西便是React.memo

React.memo是一個HOC (Higher Order Component),什麼是HOC呢?

把一個元件丟進HOC他會幫你做些事情,再還你加工過的元件;就好比高階函數 (Higher Order Function)
React 官網

React.memo 這個HOC就是幫你比較元件當中的 props 有沒有改變。

用法如下

function FancyButton(props) {
    ... 元件
}
function areEqual(prevProps, nextProps) {
    ... 根據props回傳 true / false 的函式
}
export default React.memo(FancyButton, areEqual);

/*
或是const MyComponent = React.memo(function MyComponent(props) {
    ...
});
*/

高階函數說明 (如果已經知道這部分的大大可以跳過XD)

高階函數滿足以下至少一個條件

1.是指將某個函數作為參數(arguments)給另一個函數
2.某個函數返回(return)一個函數。
【wiki】higher-order function

實際舉個常見的例子

// 1 (arguments) 匿名函式 傳入 setTimeout
    setTimeout(() => alert('Hello'), 3000)
    
// 2 (arguments) 函式 isToyota 傳入 Array.prototype.filter
    const cars = [
      { make: 'Ford', model: 'Focus' },
      { make: 'Ford', model: 'Kuga' },
      { make: 'Toyota', model: 'Wish' },
      { make: 'Toyota', model: 'Yaris' },
    ]
    const isToyota = car => car.make === 'Toyota'
    console.log(cars.filter(isToyota))
    //  [
    //    { make: 'Toyota', model: 'Wish' },
    //    { make: 'Toyota', model: 'Yaris' },
    //  ]
    
// 3 (return) 回傳出一個函式存入 addTen
    function add(x) {
      return function (y) {
        return x + y
      }
    }
    let addTen = add(10)
    addTen(7) // 17

React.memo

React.memo(CustomComponent, areEqual);,當中有兩個參數
1.需要做效能優化、比較 props 的元件本人。
2.想要比較的 props 有哪些。 如果你沒輸入,這個參數預設為比較整個 props 內所有數值。

情境

情境一

App 當中有 2 個 state ,分別是 count1, count2,各自也有顆遞增的按鈕。

          const [count1, setCount1] = useState(0);
          const [count2, setCount2] = useState(0);
          ------------------------------
          <button onClick={() => setCount1(count1 + 1)}> count1 is {count1} </button>
          <button onClick={() => setCount2(count2 + 1)}> count2 is {count2} </button>

把一個 Child 元件放入App,並且提供一個 count1 作為其 props

          const Child = ({ count }) => {
              console.log("====== Child Render ======");
              return <div>count1 is {count}</div>;
          };
          <button onClick={() => setCount1(count1 + 1)}> count1 is {count1} </button>
          <Child count={count1} />
          <button onClick={() => setCount2(count2 + 1)}> count2 is {count2} </button>

此時我們發現,不管點 count1 或 count2 的 按鈕, 因為App本身有state改變了,底下的Child都會被重新渲染
但對於 <Child count={count1} /> 而言,它關注的props只有count1而已,假如這Child的渲染很複雜,我們不希望它一直被重新渲染,我們可以使用React.memo
首先我們透過建立另外一個元件作為比較。

            const Child = ({ count }) => {
              console.log("====== Child Render ======");
              return <div>count1 is {count}</div>;
            };
            const ChildWithMemo = React.memo(({ count }) => {
              console.log("====== Child with Memo Render ======");
              return <div>count is {count}</div>;
            });
          <button onClick={() => setCount1(count1 + 1)}> count1 is {count1} </button>
          <Child count={count1} />
          <ChildWithMemo count={count1} />
          <button onClick={() => setCount2(count2 + 1)}> count2 is {count2} </button>

Demo1 Codesandbox
此時我們便可以發現,當 count2 改變時,包上memo的 <ChildWithMemo count={count1} /> 元件,不會再被重新渲染了。

情境二,如果我們只想要比較props的某個部分 / 或某個props改變,再重新渲染的話,可以使用第二個參數,或者你的props 是 物件/陣列 他們比較的是記憶體位置

App 當中有 1 個 stateperosn, person是個物件,裡面包含了 姓名 / 年齡 / 薪水,這三個屬性。
我們有兩顆按鈕,一個叫加薪、一個叫做變老 QwQ,分別會對 person 更新 年齡薪水

        const [person, setPerson] = useState({
            name: "Ken",
            age: 20,
            salary: 30000
        });
        const raiseSalary = () =>
            setPerson({ ...person, salary: parseInt(person.salary * 1.25) });
        const raiseAge = () => setPerson({ ...person, age: person.age + 1 });
        <div className="App">
            <button onClick={raiseSalary}>加薪</button>
            <button onClick={raiseAge}>變老</button>
            <div> 姓名 {person.name}</div>
            <div> 年齡 {person.age}</div>
            <DemoSalary person={person} />
        </div>
        const DemoSalary = ({ person }) => {
          console.log("====== DemoSalary render ======");
          return <div> 薪水 {person.salary}</div>;
        }

今天我們希望這有個<DemoSalary person={person} />,只有當薪水改變時,我們才會重新渲染,這時候我們就可以使用到React.memo(元件,比較函式)的第二個參數啦!當上個props跟下個props的薪水是一樣時,我們就回傳true,否則回傳false

        const isSalaryEqual = (prevProps, nextProps) =>
          prevProps.person.salary === nextProps.person.salary;

接著著再把這個function,放進React.memo的第二個參數,便大功告成啦!

        const DemoSalary = React.memo(({ person }) => {
          console.log("====== DemoSalary render ======");
          return <div> 薪水 {person.salary}</div>;
        }, isSalaryEqual);

Demo2 codesandbox link
危險~! 這邊必須注意的是,如果你僅單純比較某些 props 特定的部分,你必須確保畫面中沒有用到 props 其他部分。否則他們的畫面將不會被更新到。 假如說我 DemoSalary 當中顯示了名字,但你更改姓名時,畫面不會更新你的名字

注意事項

1. 當 props 本身很頻繁改變時,請不要使用

2. 當 props 是一個callback function 時,需要搭配 useCallback 使用 (之後會介紹)

使用時機

以下參考Use React memo Wisely

1.元件是pure functional component

pure functional 是指同一個function的input不管執行多少次,output永遠都會一樣。元件而言就是,相同的props進去幾次,畫面永遠都一樣。
因為我們優化了效能,不渲染多餘的畫面時,不能遺漏渲染某些畫面的可能性(可能元件有某些side effect),因為畫面不會重新渲染,所以我們必須保證每次同樣的props都會產生相同的畫面。

2.這個元件頻繁被render (呼應上面注意事項,不是props頻繁改變)

3.每次re-render的props幾乎都一樣

4.這個元件很大(很昂貴),因此你必須要避免它常被重新渲染

參考資料

React 性能優化那件大事,使用 memo、useCallback、useMemo
React.memo 与 useMemo ai哟 知乎
//Fooish 程式技術// React State
Javascript面面觀:核心篇《高階函數》
前端工程研究:理解函式編程核心概念與如何進行 JavaScript 函式編程
Use React memo Wisely
[譯] 詳解 React 渲染


上一篇
來個聰明的傢伙,幫我記憶一些資料吧! 出來吧 useMemo (9/19 修正描述)
下一篇
所以昨天說的那個 useCallback 是幹啥用的? 效能優化? 來瞧瞧
系列文
關於React,那些我不知道的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言