React Hooks:正確運用Memoization(記憶化)解決性能問題

React Hooks讓咱們的工做方方面面都變得更好。可是但有時候一些性能問題也是很棘手的。咱們能夠運用Hooks編寫高性能應用,可是前提是你對於下面的這些問題有清醒的認知。react

你該不應使用Memoization?

Memoization:在計算機科學中,記憶化(英語:memoization而非memorization)是一種提升程序運行速度的優化技術。經過儲存大計算量函數的返回值,當這個結果再次被須要時將其從緩存提取,而不用再次計算來節省計算時間。 記憶化是一種典型的時間存儲平衡方案。 --維基百科typescript

React在大多數使用場景中已經足夠高性能。若是你的應用足夠快而且沒有任何渲染問題,那麼就不必往下看了。不要嘗試去解決假想的渲染問題,因此在提升性能以前,確認下你是否是熟悉React Profiler數組

若是你已經明確知道加載慢的問題在哪,Memoization是最好的嘗試方法。緩存

React.memo 是提升性能工具,同時也是一個HOC(高階組件)。它和React.PureComponent很類似,只不過它應用於函數組件而不是class組件。若是函數組件根據給定的相同的props渲染相同的結果,React會記住這些,跳過渲染組件,而且複用最後一次渲染結果。函數

默認狀況下它會對複雜的props對象進行淺比較。若是你想要控制比較過程,也能夠提供一個自定義的比較函數做爲鉤子函數的第二個參數。工具

無Memoization:

咱們來舉一個不使用Memoization的例子,而後看爲何這樣會形成問題。性能

function List({ items }) {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
}export default function App() {
  log('renderApp');      function List({ items }) {
        log('renderList');
        return items.map((item, key) => <div key={key}>item: {item.text}</div>);
    }
    export default function App() {
        log('renderApp');
        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));
        return (
            <div>
                <h1>{count}</h1>
                <button onClick={() => setCount(count + 1)}>inc</button>
                <List items={items} />
            </div>
        );
    }
	const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}
複製代碼

NO1:Live Demo

每次點擊increnderApprenderList都會打印,甚至List中沒有任何改變。若是這棵數據樹足夠大,那麼它會很容易引起性能問題。咱們須要減小渲染次數。flex

簡單Memoization

const List = React.memo(({ items }) => {
        log('renderList');
        return items.map((item, key) => <div key={key}>item: {item.text}</div>);
    });
    export default function App() {
        log('renderApp');
        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));
        return (
            <div>
                <h1>{count}</h1>
                <button onClick={() => setCount(count + 1)}>inc</button>
                <List items={items} />
            </div>
        );
    }
複製代碼

NO2:Live Demo

在這個例子中Memoization作了不少工做,而且減小了不少渲染次數。在加載過程當中renderApprenderList都打印了,可是點擊inc的時候只有renderApp打印了。優化

Memoization & callback

咱們來作一個小修改,在全部List條目中添加上inc按鈕。記住,向組件中傳入回調函數來記憶化組件會形成微妙的bugspa

function App() {
        log('renderApp');

        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));

        return (
            <div>
                <div style={{ display: 'flex' }}>
                    <h1>{count}</h1>
                    <button onClick={() => setCount(count + 1)}>inc</button>
                </div>
                <List items={items} inc={() => setCount(count + 1)} />
            </div>
        );
    }
複製代碼

NO3:Live Demo

在這個例子中,咱們的Memoization失敗了。由於咱們使用了內聯匿名函數,每次渲染的時候都會生成新的渲染結果,這樣React.mome就失效了。因此在咱們記住這個組件以前,咱們要想辦法記住這個函數。

useCallback

十分幸運的是,React有兩個內建鉤子能夠提供上面的功能:useMemouseCallbackuseMemo對於複雜計算頗有用,useCallback對於傳遞迴調函數給須要提升性能的子組件十分有用。

function App() {
        log('renderApp');

        const [count, setCount] = useState(0);
        const [items, setItems] = useState(getInitialItems(10));

        const inc = useCallback(() => setCount(count + 1));

        return (
            <div>
                <div style={{ display: 'flex' }}>
                    <h1>{count}</h1>
                    <button onClick={inc}>inc</button>
                </div>
                <List items={items} inc={inc} />
            </div>
        );
    }
複製代碼

NO4:Live Demo

在這個例子中,咱們的Memoization又失敗了。renderList在每次inc被點擊的時候都會調用。useCallback的默認行爲是函數實例每次掛載的時候都計算新值。由於內聯匿名函數每次渲染的時候都會建立新實例,默認設置下useCallback無效。

useCallback with input

const inc = useCallback(() => setCount(count + 1), [count]);

NO5:Live Demo

useCallback接收的第二個參數是一個輸入變量數組,當且僅當這些輸入變量變化的時候,useCallback會返回新值。在這個例子中,當count變化的時候useCallback會返回新值。每次count在渲染的時候改變,useCallback都會跟着改變。這樣也沒有實現很好的記憶功能。

useCallback with input of empty array

const inc = useCallback(() => setCount(count + 1), []);

NO6:Live Demo

useCallback也能夠接收一個空數組做爲第二個參數,這樣內聯匿名函數就只會調用一次。此次代碼實現了記憶功能,點擊按鈕會打印renderApp,主要按鈕的功能也正常,可是內部inc按鈕中止正常工做了。

計數器從0增長到1後就中止工做了。匿名函數被建立一次,調用了屢次。由於匿名函數被建立的時候count是0,他的表現就像下面這樣: const inc = useCallback(() => setCount(1), []); 這個問題的根本緣由在於,咱們試圖同時讀寫state。幸運的是,react提供瞭解決上面問題的方法:

useState with functional updates

const inc = useCallback(() => setCount(c => c + 1), []);

NO7:Live Demo

useState返回的setters能夠將函數做爲參數,這個函數中能夠讀取上一state的值。在這個例子中,Memoization正確工做,沒有bug。

useReducer

const [count, dispatch] = useReducer(c => c + 1, 0);

NO8:Live Demo

useReducer memorizationuseState中的例子同樣正確運行了。由於dispatch要保證渲染過程當中參考對象一致,就不須要useCallback了,dispatch使得代碼更少的犯Memoization相關錯誤。

useReducer vs useState

useReducer 更適合管理複雜對象或者下一state依賴上一state的狀況。使用useReducer的常見模式是和useContext一塊兒使用,避免在大型組件樹中顯示傳遞迴調。 我比較推薦用useState來管理組件內部數據,useReducer適合管理須要在父子組件中傳遞的特殊雙向數據。

總之,React.memouseReducer是最好的朋友;React.memouseState是時而產生衝突的兄弟,會形成一些問題;謹慎使用useCallback

生活的一部分是工做,工做的一部分是解決問題取悅生活,因此好好生活,好好工做,好好熱愛(●ˇ∀ˇ●)

原文地址

相關文章
相關標籤/搜索