iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 20
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 20

【Day.20】React效能 - 用useCallback避免函式的重新定義

在前面,我們說在非必要的時候,不要在React function component內宣告函式。

那什麼時候是「必要的時候」呢 ?

當我們有需要綁定和state、props或React hook有關的東西時候。

舉例來說,我們前面有提過可以用useRef當作counter,這個時候如果我們希望透過MenuItem被點擊的時候,印出counter的值,就要這樣寫:

  • src/component/MenuPage.js
import React, {useState,useRef} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);

    /* 定義counter */
    const renderCounter = useRef(0);
    renderCounter.current++;

    /* 定義列印函式 */
    const handleClick = () => {
        console.log("counter is " + renderCounter.current);
    }

    /* 綁定函式到元素上 */ 
    let menuItemArr = menuItemWording.map((wording) =>{
        return <MenuItem text={wording} handleClick={handleClick}/>
    });

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
        </OpenContext.Provider>
    );
}

export default MenuPage;
  • src/component/MenuItem.js
import React, {memo, useContext} from 'react';
import { OpenContext } from '../context/ControlContext';

const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};

function MenuItem(props){
    //const isOpenUtil = useContext(OpenContext);
    //const func = isOpenUtil;
    
    /* 綁定handleClick */
    return <li 
                style={menuItemStyle} 
                onClick={()=>{props.handleClick()}}
            >
                {props.text}
            </li>;
}

export default memo(MenuItem);

理論上因為我們的props中wording沒有變,handleClick也因為牽涉的變數使用了useRef,定義也沒有變。根據上一篇我們對於memo的解說,此時memo會幫助我們不重新渲染元件。

然而你打開profiler,卻會看到以下結果:

為什麼會這樣?

這是因為memo在比較props的時候,當遇到物件類別,會去比較它的reference,而不是一一比對物件當中的屬性。前面提過,當function component被重新渲染時會呼叫整個元件函式的定義域。由於函式也是物件的一種,這裡MenuPage的handleClick在重新渲染時也被重新宣告了一次,導致reference改變,MenuItem的memo就會覺得props被改變了。

useCallback

useCallback就是能夠用來解決這件事情的React hook。它很像是「專為函式定義用的useRef」,可以幫我們確保函式非必要時不會被重新定義,也就是reference不變。

它的語法跟useEffect很像:

const func = useCallback(定義函式,[相依變數]);

舉例來說,剛剛的MenuPage只要改成這樣,MenuItem就不會被重新渲染:

import React, {useState, useRef, useCallback} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);

    const renderCounter = useRef(0);
    renderCounter.current++;

    const handleClick = useCallback(() => {
        console.log("counter is " + renderCounter.current);
    },[renderCounter]);

    let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording} handleClick={handleClick}/>);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
        </OpenContext.Provider>
    );
}

export default MenuPage;

但是這樣useCallbackuseRef的差別在哪裡呢?

還記得剛剛介紹語法時的第二個參數「相依變數」嗎? useCallback可以讓我們用這個第二個參數,來設定哪些東西被改變時,要去重新定義函式

以下面的例子來說,如果我們把handleClick改成印出isOpen的值:

    const handleClick = useRef(() => {
        console.log("isOpen is " + isOpen);
    });

你會發現上面這個函式永遠只會印出isOpen在第一次渲染的初始值。這是因為isOpen的值在定義函式時,原始值就被填進去了,之後由於我們沒有去特別更新useRef的ref.current,所以定義始終沒有被改變。

這個時候改成使用useCallback就會像這樣:

    const handleClick = useCallback(() => {
        console.log("isOpen is " + isOpen);
    },[isOpen]);

此時你就會發現函式印出來的值跟isOpen一樣了。不過也因為定義被改變了,這個時候MenuItem的重新渲染就不可避免。

使用useCallback也要注意定義中的side Effect

和useEffect一樣,React會希望useCallback中定義的內容只和第二個參數中有被定義的東西有關。當你在useCallback中使用了state、props、React hook相關的東西而又沒有定義在第二個參數時,React也會跳warning。


上一篇
【Day.19】React效能 - 用memo避免不必要的重複渲染
下一篇
【Day.21】React效能 - 用useMemo避免函式非必要的執行
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言