suemor

suemor

前端萌新
telegram
github
twitter

React 中的 memo、useMemo 及 useCallback

距離我接觸 react 已經過去幾個月了,在此期間,關於如何避免重複渲染的問題一直困惑著我,因此今天就來聊聊這個話題。

在講述如何進行性能優化之前,我們先來談談 React 為什麼會重新渲染。

React 為什麼會重新渲染#

狀態改變是 React 樹內部發生更新的唯二原因之一

import { useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  console.log('render');
  return <p>I am a very slow component tree.</p>;
};

input 例子

很明顯,每當我們在 input 裡面輸入內容,console.log('render')都會輸出,因為 color 的狀態發生了改變,也間接說明了其與 props 完全沒有關係

這樣的開銷是很不合理的,我們接下來會優化它。

性能優化#

方式一: State 抽離#

我們知道 react 是單向數據流,因此我們只需要將 State 的抽離出來即可。

import { useState } from "react";

const App = () => {
  return (
    <div>
      <Input />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  console.log("render");
  return <p>I am a very slow component tree.</p>;
};

const Input = () => {
  let [color, setColor] = useState("red");

  return <input value={color} onChange={(e) => setColor(e.target.value)} />;
}

方式二: memo#

React.memo 其為高階組件,可以使被它包裹的組件變為純組件,也就是只要它的 prop 不改變,react 就不會更新它。

import { memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = memo(() => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  return <p>I am a very slow component tree.</p>;
})

方式三: react children#

因為 App 並沒有發生狀態改變所以 ExpensiveTree 避免了重複渲染

import { FC, PropsWithChildren, useState } from "react";

const App = () => {
  return (
    <ColorWrapper>
      <ExpensiveTree />
    </ColorWrapper>
  );
};

const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  return <p>I am a very slow component tree.</p>;
};

使用 useMemo 和 useCallback#

useMemo#

useMemo 有點類似於 vue 中的 Computed,只有當依賴變化時,才會重新計算出新的值。

這樣當 input 發生改變的時候 dirtyWork 就不會重複的去執行。

import { useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const [number,setNumber] = useState(0)
  
  const dirtyWork = useMemo(() => {
    console.log('正在進行大量運輸');
    return number
  },[number])
  
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <h1>{dirtyWork}</h1>
    </div>
  );
};

另外上一節的例子我們也可以透過 useMemo 進行修改

import { memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {useMemo(
        () => (
          <ExpensiveTree />
        ),
        []
      )}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  return <p>I am a very slow component tree.</p>;
};

useCallback#

我們先看如下例子

import { FC, memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = ()=> {
    console.log('hahaha');
  }
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  console.log('render'); // 依舊會被不斷更新
  return <p>I am a very slow component tree.</p>;
})

我們發現即使 ExpensiveTree 包裹 memo ,但在 input 裡面輸入內容, ExpensiveTree 依舊會被更新,這時我們只要給父組件 fn 函數包裹一層 useCallback 即可

因此 useCallback 一般用於需要將函數傳遞給子組件的情況,我們用 useCallback 改寫上面的例子:

import { FC, memo, useCallback, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useCallback(()=> {
    console.log('hahaha');
  },[])
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  console.log('render');
  return <p>I am a very slow component tree.</p>;
})

你可能會發現 useCallback 其實就是 useMemo 的語法糖,如上例子也可以使用 useMemo 改寫

import { FC, memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useMemo(() => {
    return () => console.log("hahaha");
  }, []);
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn} />
    </div>
  );
};

const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 延遲
  }
  console.log("render");
  return <p>I am a very slow component tree.</p>;
});

參考資料#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。