React 的頁面是由很多個 Component 堆疊而成,在前面的文章可以發現,要讓資料與畫面連動的方式就是觸發 Render,讓畫面重新渲染。
使用少量的元件時,畫面重新渲染不會有太大的問題,一旦網站架構變得龐大且複雜時,大量的元不斷的重新被渲染,可能會給瀏覽器帶給相當大的負擔,也容易造成在使用網站時的 UX 體驗不好。
Re-Render 造成的影響
要減少上述的影響,就是物件要能夠被 Memorize (記住)。
React 有兩個 Hook 可以做 Memorize - useMemo、useCallback,它們讓 React 可以記住 Object 的記憶體位址,而不會重新執行不該被再次執行的運算。
讓我們來觀察以下二個範例的不同
function App() {
console.log('render');
const [value, setValue] = React.useState(false);
const aa = 100;
React.useEffect(() => {
console.log('useEffect cb');
}, [aa]);
return <>
<h1>App</h1>
<button onClick={() => {setValue(!value)}}>重新Render</button>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/BaxpaqG?editors=0011
function App() {
console.log('render');
const [value, setValue] = React.useState(false);
const obj = { name: 'bruce', data: {} };
React.useEffect(() => {
console.log('useEffect cb');
}, [obj]);
return <>
<h1>App</h1>
<button onClick={() => {setValue(!value)}}>重新Render</button>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/gOzgyGq?editors=0011
使用 useMemo 來記憶物件,useEffect 的 deps 若是 useMemo 後的物件,Re-Render 時,因為物件有被記憶住,所以不會重新執行 useEffect。
function App() {
console.log('render');
const [value, setValue] = React.useState(false);
// 記住回傳物件值的useMemo
const memoObj = React.useMemo(() => {
const obj = { name: 'bruce', data: {} };
return obj
}, [])
React.useEffect(() => {
console.log('useEffect cb');
}, [memoObj]);
return <>
<h1>App</h1>
<button onClick={() => {setValue(!value)}}>重新Render</button>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/NWMdZpz?editors=0011
如果 useEffect 的 deps 是 Function,而 Function 視為是物件的一種,除了可以使用 useMemo 來記憶,也可以使用 useCallback。
useCallback 是 useMemo 的一種變體,用來記住 Function,useCallback 其實就等於回傳一個 Function 的 useMemo。
使用 useMemo 來記憶 Function (視為物件),useEffect 的 deps 若是 useMemo 後的物件,重新 Render 時,因為物件有被記憶住,所以不會重新執行 useEffect
function App() {
console.log('render');
const [value, setValue] = React.useState(false);
// 記住 回傳物件值的 useMemo,這邊回傳的是 Function
const memoFunction = React.useMemo(() => {
return () => {} // Retun Function Object
}, [])
React.useEffect(() => {
console.log('useEffect cb');
}, [memoFunction]);
return <>
<h1>App</h1>
<button onClick={() => {setValue(!value)}}>重新Render</button>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/OJZWeoE?editors=0011
使用 useCallback 來記憶 Functon,useEffect 的 deps 若是useCallback 後的 Function,重新 Render 時,因為 Function 有被記憶住,所以不會重新執行 useEffect
function App() {
console.log('render');
const [value, setValue] = React.useState(false);
// 記住 Function 的 useCallback
const memoFunction = React.useCallback(
() => { console.log('memoFunction here'); }
, [])
React.useEffect(() => {
console.log('useEffect cb');
memoFunction();
}, [memoFunction]);
return <>
<h1>App</h1>
<button onClick={() => {setValue(!value)}}>重新Render</button>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/LYmxwNz?editors=0011
const memoizedValue = useMemo(callback-fn, dependency-array);
dependency-array
只有這個陣列裡的變數更新,才會執行 useMemo 的 callback function,若陣列為空則表示初始執行一次。callback-fn
通常會放需要複雜計算的邏輯處理,讓它只有 dependency-array 的變數更新時才執行。const memoizedValue = useMemo(() => {
let result = xxxxx;
// 在此處放置複雜計算的邏輯處理
// ...
return result; // 要回傳給 memoizedValue 的值
}, [obj1, obj2, ...]);
function App() {
console.log('render');
// status 變動,雖然會觸發 Render,但 count 沒有變動不會執行 slowComputed
const [status, setStatus] = React.useState(false);
const [count, setCount] = React.useState(1);
// 只有 count 變動時,才會執行 slowComputed
const slowComputed = React.useMemo(() => {
console.log('slowComputed');
let sum = 0;
for (let i = 0; i < count * 100 ; i++) {
sum += 1;
}
return sum;
}, [count])
return <>
<div>Status:{String(status)}</div>
<div>Count:{count}</div>
<div>SlowComputed:{slowComputed}</div>
<div>
<button onClick={() => {setStatus(!status)}}>Change Status</button>
<span> Status 變動,雖然會觸發 Render,但不會執行 slowComputed</span>
</div>
<div>
<button onClick={() => {setCount(count+1)}}>Change Count</button>
<span> Count 變動,會觸發 Render,也會執行 slowComputed</span>
</div>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/yLjMJOR?editors=0011
function App() {
console.log('render');
// status 變動,雖然會觸發 Render,但 dark 沒有變動不會執行 useEffect
const [status, setStatus] = React.useState(false);
// 只有 dark 變動時,才會執行 useEffect
const [dark, setDark] = React.useState(false);
const themeStyle = React.useMemo(() => {
return {
backgroundColor: dark ? 'black' : 'white',
color: dark ? 'white' : 'black',
}
}, [dark]);
React.useEffect(() => {
console.log('useEffect callback')
}, [themeStyle]);
return <>
<div>Status:{String(status)}</div>
<div>Dark:{String(dark)}</div>
<div>ThemeStyle:{JSON.stringify(themeStyle)}</div>
<div>
<button onClick={() => {setStatus(!status)}}>Change Status</button>
<span> Status 變動,雖然會觸發 Render,但不會執行 useEffect</span>
</div>
<div>
<button onClick={() => {setDark(!dark)}}>Change Dark</button>
<span> Dark 變動,會觸發 Render,也會執行 useEffect</span>
</div>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/vYjxeyw?editors=0011
const memoizedCallback = useCallback(callback-fn, dependency-array);
dependency-array
只有這個陣列裡的變數更新,才會執行 useCallback 的 callback function,若陣列為空則表示初始執行一次。callback-fn
要被記住的函式,讓它只有 dependency-array 的變數更新時才執行。使用 useMemo 需要有回傳值的表達式,無論回傳的是 Object 或是 Function
使用 useCallback 不需要有回傳值的表達式,傳入的 Callback Function,就是要被記住的 Function
useCallback(fn, deps) 等同於 useMemo(() => fn, deps)
function App() {
console.log('render');
const [postId, setPostId] = React.useState(1);
const [status, setStatus] = React.useState(false);
const fetchDataWithUseCallback = React.useCallback(() => {
const fetchingData = async () => {
const fetchUrl = `https://jsonplaceholder.typicode.com/posts/${postId}`;
const response = await fetch(fetchUrl);
const data = await response.json(response);
return data;
};
fetchingData();
}, []);
const fetchData = async () => {
const fetchUrl = `https://jsonplaceholder.typicode.com/posts/${postId}`;
const response = await fetch(fetchUrl);
const data = await response.json(response);
return data;
};
React.useEffect(() => {
console.log('execute fetchDataWithUseCallback in useEffect');
fetchDataWithUseCallback();
}, [fetchDataWithUseCallback]);
React.useEffect(() => {
console.log('execute fetchData in useEffect');
fetchData();
}, [fetchData]);
return <>
<div>Status:{String(status)}</div>
<div>
<button onClick={() => {setStatus(!status)}}>Change Status</button>
<span> Status 變動,雖然會觸發 Render,但不會執行 useEffect</span>
</div>
</>
}
執行結果:https://codepen.io/lala-lee-jobs/pen/xxjqPRK?editors=0011
這個會在下一篇介紹 React.memo 的時候會一併說明
useMemo 與 useCallback 都會用來「避免重新執行不該被再次執行的運算」,接下來要介紹的 React.memo 則是用以「減少不必要被更新的元件被重新渲染」。
https://zh-hant.reactjs.org/docs/hooks-reference.html#usememo
https://zh-hant.reactjs.org/docs/hooks-reference.html#usecallback
https://ithelp.ithome.com.tw/articles/10269673
https://hackmd.io/@yellow/react-hooks#useCallback-%E8%A8%98%E6%86%B6%E5%80%BC
https://ithelp.ithome.com.tw/m/articles/10270317
https://juejin.cn/post/7122027852492439565
https://ithelp.ithome.com.tw/articles/10225504