新年開篇:如何減少react元件不必要的重新渲染

語言: CN / TW / HK

theme: smartblue

什麼是重新渲染

react重新渲染是當你的元件已經渲染在螢幕上,由於資料狀態變化,又重新觸發了元件的渲染。重新渲染是正常現象,但有時由於自身程式碼問題造成不必要的重新渲染。

不必要的重新渲染

由於錯誤的程式碼引發元件的重新渲染。例如:與react元件本身無關的狀態變化引起的元件的重新渲染。雖然react元件重新渲染了,但由於渲染很快,通常使用者並不會感知到。但如果重新渲染髮生比較複雜的元件上,可能會導致介面卡頓,甚至造成長時間的卡死現象。

react元件重新渲染情況

元件的重新渲染分為以下幾種情況:

state變化

count變化會引起元件的重新渲染。

``` const App = () => { const [count, setCount] = useState(0);

const handleClick = () => { setCount(count+1) }

return

{count}
} ```

父元件的重新渲染

當元件的父元件重新渲染時,該元件必定重新渲染。一般情況下子元件的渲染不會觸發父元件的渲染。

實際上props變化也是由於父元件狀態變化引起的,只要父元件重新渲染,子元件不管props有沒有變化都會重新渲染。(沒有使用優化時)

``` import { useState } from "react";

const Child = () => { console.log("子元件重新渲染"); const [childCount, setChildCount] = useState(0);

const handleChildClick = () => { setChildCount(childCount + 1); };

return ( <>

Child {childCount}

); };

export default function StateChange() { const [count, setCount] = useState(0);

const handleClick = () => { setCount(count + 1); };

console.log("父元件重新渲染", count);

return (

parent {count}
); } ```

context變化

當context的Provider提供的值放生變化,所有使用該context的元件都將重新渲染。

``` import { createContext, useState, useContext, useMemo } from "react";

const Context = createContext({ val: 0 });

const Provider = ({ children }) => { const [val, setVal] = useState(0); const handleClick = () => { setVal(val + 1); };

const value = useMemo(() => { return { val: val }; }, [val]);

return ( {children} ); };

const useVal = () => useContext(Context);

const Child1 = () => { const { val } = useVal(); console.log("Child1重新渲染", val);

return

Child1
; };

const Child2 = () => { const { val } = useVal(); console.log("Child2重新渲染", val); return

Child2
; };

export default function ContextChange() { return ( ); } ```

在元件內建立元件

❌:這種做法非常消耗效能。當元件重新渲染,內部元件都會重新mount。這種做法容易導致很多bug出現。

``` import { useState, useEffect } from "react";

const Component = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

const InComponent = () => { console.log("內部元件重新渲染"); useEffect(() => { console.log("內部元件重新mount"); }, []); return

inComponent
; };

return ( <>
); };

export default function ComInComponent() { return ( <>
); } ```

一些減少重新渲染的方法

useState初始值使用函式形式

看一個例子:useState的初始值經過a + 1計算得到: 此時的useState的引數是一個函式執行,也就是一個值

``` import { useState } from "react";

function getInitState() { console.count("獲取初始化的值"); const a = 1; return a + 1; }

const App = () => { const [value, setValue] = useState(getInitState()); const onChange = (event) => setValue(event.target.value);

return ; };

export default App; ```

當我們在input中繼續輸入,可以看到getInitState的console次數:

當我們把useState第一個引數改為函式時:(該函式會自動呼叫)

``` import { useState } from "react";

function getInitState() { console.count("獲取初始化的值"); const a = 1; return a + 1; }

const App = () => { const [value, setValue] = useState(getInitState); const onChange = (event) => setValue(event.target.value);

return ; };

export default App; ```

該函式只會在初始化的時候呼叫一次:

重新組織元件結構

提取單獨的元件,獨自維護自己的狀態,防止發生不必要的渲染。

``` import { useState } from "react";

const BigComponent = () => { console.log("一個非常複雜的大元件:渲染了"); return

BigComponent
; };

const AllComponent = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}


); };

const ButtonComponent = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}


); };

const SplitComponent = () => { return ( <>
); };

const App = () => { return ( <>

AllComponent是沒有重新組織的元件


SplitComponent是重新組織劃分的元件, 不會觸發大元件BigComponent的渲染


); };

export default App; ```

巧用props.children

利用props.children 來減少不必要的重複渲染。

``` import { useState } from "react";

const BigComponent = () => { console.log("一個非常複雜的大元件:渲染了"); return

BigComponent
; };

const AllComponent = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}


); };

const ComponentWithChildren = ({ children }) => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}

{children}
); };

const SplitComponent = () => { return ( ); };

const App = () => { return ( <>

AllComponent是沒有重新組織的元件


SplitComponent是巧用了children的元件, 不會觸發大元件BigComponent的渲染


); };

export default App; ```

把元件當成props傳遞

把元件當成props傳遞給其他元件,也可以減少渲染。(與children類似)

``` import { useState } from "react";

const BigComponent = () => { console.log("一個非常複雜的大元件:渲染了"); return

BigComponent
; };

const AllComponent = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}


); };

const ComponentWithProps = ({ comp }) => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

return ( <>

重新渲染次數: {state}

{comp}
); };

const comp = ;

const SplitComponent = () => { return ( <>
); };

const App = () => { return ( <>

AllComponent是沒有重新組織的元件


SplitComponent是把元件當成props傳遞, 不會觸發大元件BigComponent的渲染


); };

export default App; ```

React.memo

memo 減少重複渲染,只要子元件的props沒有改變(淺比較)。

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

const Child = () => { console.log("子元件重新渲染");

return ( <>

Child

); };

const MemoChild = memo(Child);

export default function ReactMemo() { const [count, setCount] = useState(0);

const handleClick = () => { setCount(count + 1); };

return (

parent {count}
); } ```

如果props是一個object, array 或者 function,memo必須配合useMemo來快取,單獨使用都不起作用。

當然也可以單獨使用useMemo快取整個元件。

``` import React, { useState, useMemo } from "react";

const Child = ({ value }) => { console.log("Child重新渲染", value.value); return <>{value.value}; };

const values = [1, 2, 3];

const UseMemp = () => { const [state, setState] = useState(1);

const onClick = () => { setState(state + 1); };

// 使用useMemo快取元件 const items = useMemo(() => { return values.map((val) => ); }, []);

return ( <>
{items}
); };

export default UseMemp; ```

useCallback

函式傳遞可以使用useCallback來快取函式。

key值

在迴圈陣列時,有人使用index作為key,可以這樣做,但是你要保證你的陣列是靜態的,沒有新增,刪除,插入,排序等情況,否則不能使用index作為key。

完整的程式碼:

  1. https://codesandbox.io/s/hidden-meadow-kksw2o?file=/src/App.js
  2. https://mp.weixin.qq.com/s/wJwHbPf7_L0p3TwCjzUANA

參考

  1. https://reactjs.org/docs/hooks-faq.html
  2. https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928