React(12) - useCallback & useMemo


Posted by TempuraEngineer on 2022-11-17

目錄


memorized hook是甚麼

memorized hook接受2個參數,第一個是函式,第二個式依賴陣列(dependency array)

它會記住依賴陣列的狀態,當依賴陣列內的值更新時才會呼叫callback

因此常用於避免不必要的渲染、計算


useCallback & memo

兩者的功能類似,只差在memo是用shallow compare的方式在做比較

memo包住的組件在其父組件更新,但傳給它的props沒更新時不會重新渲染

useCallback記住的回傳函式,就能避免每次產生新的function(重新分配記憶體),又傳給子組件

前者比較props有無改變來避免重新渲染,後者避免重複分配記憶體

兩者常一起用避免父組件state更新而重新渲染時,props沒更新,但卻連子組件也重新渲染不必要的渲染會衝擊效能

function GeneralChild({callback, type, children}){
  console.log(`G + ${type[0]}`);

  return (
    <div className="border-solid border-2 border-blue-400 p-2">
        <h1 className={type.startsWith('general')? 'bg-blue-400' : 'bg-yellow-400'}>G + {type[0]}</h1>
        {children}
        <p>callback return value: {callback()}</p>
    </div>
  )
}

const MemoizedChild = React.memo(({ callback, type, children }) => {
  console.log(`M + ${type[0]}`);

  return (
    <div className="border-solid border-2 border-yellow-400 p-2">
      <h1 className={type.startsWith('general')? 'bg-blue-400' : 'bg-yellow-400'}>M + {type[0]}</h1>
      {children}
      <p>callback return value: {callback()}</p>
    </div>    
  )
});

function App(){
  const [num, setNum] = useState(1);

  // 將callback寫死
  const memoizedCallback = useCallback(() => {
    return 33;
  }, []);

  const generalCallback = () => {
    return 33;
  }

  console.log('-----------------');

  return(
    <div>
      <div className='flex mb-3'>
        <p className='m-2'>num: {num}</p>
        <Button variant="contained" onClick={() => setNum(Math.floor(Math.random() * 10) + 1)}>change num</Button>
      </div>

      <div className='grid grid-cols-2 gap-2'>
        {/* num變,即便callback回傳值寫死,子組件會重新render */}
        <GeneralChild type='memoized callback' callback={memoizedCallback}></GeneralChild>

        {/* num變,即便callback回傳值寫死,子組件會重新render */}
        <GeneralChild type='general callback' callback={generalCallback}></GeneralChild>

        {/* 子組件不重新render */}
        <MemoizedChild type='memoized callback' callback={memoizedCallback}></MemoizedChild>

        {/* num變,即便callback回傳值寫死,子組件會重新render */}
        <MemoizedChild type='general callback' callback={generalCallback}></MemoizedChild>
      </div>
    </div>
  );
}

先來看看useCallback記住回傳函式的特點

const set = new Set();
const arr = [];

function GeneralChild({callback, type, children}){
  console.log(`G + ${type[0]}`);
  store.add(callback);
  arr.push(callback);
  console.log(store, arr);

  // 後略
}

set會排除同樣的元素,如果是物件型別則根據記憶體位置判斷是否相同

使用useCallback回傳的函式做為prop

pass a function return by useCallback as props

不用的話

pass a function as props

再來看避免子組件不必要的渲染

useCallback

從上圖可以看到如果沒有使用memo + useCallback,即便傳給callback prop的函式被寫死了,只要父組件的num被更新了,子組件就會被重新渲染


useMemo

與父組件狀態更新無關,而是避免組件重新渲染時,組件內的函式被重新呼叫

重新呼叫函式造成不必要的重新計算衝擊效能

no useMemo

import {useReducer, useMemo} from 'react';

function reducer(state, action){
    const res = {...state};
    const {type:key, value} = action;

    const isArray = state[key] instanceof Array;

    res[key] = isArray? [...res[key], value] : value;

    return res;
}

export default function ClassMemberList(){
    const [state, dispatch] = useReducer(reducer, {
        classList:['A', 'B', 'C'],
        studentList:[
            {id:'001', className:'A', name:'Alex'},
            {id:'002', className:'C', name:'Allen'},
            {id:'003', className:'B', name:'Amy'},
            {id:'004', className:'C', name:'Apollo'},
            {id:'005', className:'C', name:'Bill'},
            {id:'006', className:'C', name:'Belinda'},
            {id:'007', className:'A', name:'Cinderella'},
            {id:'008', className:'B', name:'Danial'},
            {id:'009', className:'A', name:'Emma'},
        ],
        className:'A',
        newStudent:{id:'', className:'', name:'',}
    })

    const filterClassMember = () => {
        console.log('filterClassMember', state.studentList.length);
        return state.studentList.filter(i => i.className === state.className);
    }

    const classMember = filterClassMember();

    const addStudent = () => {
        const id = (parseInt(state.studentList[state.studentList.length - 1].id) + 1).toString().padStart(3, '0');
        const isNewClassName = state.classList.indexOf(state.newStudent.className) === -1;

        dispatch({type:'studentList', value:{id, ...state.newStudent}});
        dispatch({type:'newStudent', value:{id:'', className:'', name:'',}});

        if(isNewClassName){
            dispatch({type:'classList', value:state.newStudent.className});
        }
    }

    return (
        <div className='flex m-3'>
            <div className='flex flex-col mr-16'>
                name:<input type="text" 
                            onChange={(e) => dispatch({type:'newStudent', value:{...state.newStudent, name:e.target.value}})} 
                            value={state.newStudent.name}/>
                class name:<input className='mb-2' 
                                type="text" 
                                onChange={(e) => dispatch({type:'newStudent', value:{...state.newStudent, className:e.target.value}})}
                                value={state.newStudent.className}/>

                <button className='p-2 bg-blue-400' onClick={addStudent}>add student</button>
            </div>

            <div>
                <select className='mb-2' onChange={(e) => {dispatch({type:'className', value:e.target.value})}}>
                    {state.classList.map(i => <option value={i} key={i}>{i} class</option>)}
                </select>



                <h1 className='text-2xl mb-2'>class {state.className} student list</h1>

                <ul>
                    {
                        classMember.map(i => <li key={i.id}>{i.name}</li>)
                    }
                </ul>
            </div>
        </div>
    )
}

useMemo會記住的回傳值,如果依賴陣列中的變數都沒有被更新,就使用原本的回傳值,類似Vue的computed

useMemo

    // 前略

    // 使用useMemo()取代filterClassMember
    const classMember = useMemo(() => {
        console.log('useMemo', state.studentList.length);
        return studentList.filter(i => i.className === className);
    }, [studentList, className]);

    // 後略


參考資料

React - useCallback
React - useMemo
React 性能優化那件大事,使用 memo、useCallback、useMemo


#React #useMemo #useCallback #React.memo







Related Posts

React 按下編輯按鈕後,想要自動 focus 在內容上

React 按下編輯按鈕後,想要自動 focus 在內容上

[css] css 界的人生重來槍 - revert

[css] css 界的人生重來槍 - revert

D8_判斷式

D8_判斷式


Comments