在編寫 React Hook 代碼時,useCallback
和useMemo
時常使人感到困惑。儘管咱們知道他們的功能都是作緩存並優化性能,可是又會擔憂由於使用方法不正確致使負優化。本文將闡述useCallback
和useMemo
在開發中常見的使用方式和誤區,並結合源碼剖析緣由,知其然並知其因此然。javascript
useCallback
考察以下示例:html
import React from 'react';
function Comp() {
const onClick = () => {
console.log('打印');
}
return <div onClick={onClick}>Comp組件</div>
}
複製代碼
當Comp
組件自身觸發刷新或做爲子組件跟隨父組件刷新時,咱們注意到onClick
會被從新賦值。爲了"提高性能",使用useCallback
包裹onClick
以達到緩存的目的:java
import React, { useCallback } from 'react';
function Comp() {
const onClick = useCallback(() => {
console.log('打印');
}, []);
return <div onClick={onClick}>Comp組件</div>
}
複製代碼
那麼問題來了,性能到底有沒有得到提高?答案是非但沒有,反而不如之前了;咱們改寫代碼的邏輯結構以後,緣由就會很是清晰:react
import React, { useCallback } from 'react';
function Comp() {
const onClick = () => {
console.log('打印');
};
const memoOnClick = useCallback(onClick, []);
return <div onClick={memoOnClick}>Comp組件</div>
}
複製代碼
每一行多餘代碼的執行都產生消耗,哪怕這消耗只是 CPU 的一丁點熱量。官方文檔指出,無需擔憂建立函數會致使性能問題,因此使用useCallback
來改造該場景下的組件,咱們並未得到任何收益(函數仍是會被建立),反而其帶來的成本讓組件負重(須要對比依賴是否發生變化),useCallback
用的越多,負重越多。站在 javascript 的角度,當組件刷新時,未被useCallback
包裹的方法將被垃圾回收並從新定義,但被useCallback
所製造的閉包將保持對回調函數和依賴項的引用。緩存
useCallback
的正確使用方法產生誤區的緣由是useCallback
的設計初衷並不是解決組件內部函數屢次建立的問題,而是減小子組件的沒必要要重複渲染。實際上在 React 體系下,優化思路主要有兩種:閉包
因此考察以下場景:函數
import React, { useState } from 'react';
function Comp() {
const [dataA, setDataA] = useState(0);
const [dataB, setDataB] = useState(0);
const onClickA = () => {
setDataA(o => o + 1);
};
const onClickB = () => {
setDataB(o => o + 1);
}
return <div> <Cheap onClick={onClickA}>組件Cheap:{dataA}</div> <Expensive onClick={onClickB}>組件Expensive:{dataB}</Expensive> </div>
}
複製代碼
Expensive
是一個渲染成本很是高的組件,但點擊Cheap
組件也會致使Expensive
從新渲染,即便dataB
並未發生改變。緣由就是onClickB
被從新定義,致使 React 在 diff 新舊組件時,斷定組件發生了變化。這時候useCabllback
和memo
就發揮了做用:工具
import React, { useState, memo, useCallback } from 'react';
function Expensive({ onClick, name }) {
console.log('Expensive渲染');
return <div onClick={onClick}>{name}</div>
}
const MemoExpensive = memo(Expensive);
function Cheap({ onClick, name }) {
console.log('cheap渲染');
return <div onClick={onClick}>{name}</div>
}
export default function Comp() {
const [dataA, setDataA] = useState(0);
const [dataB, setDataB] = useState(0);
const onClickA = () => {
setDataA(o => o + 1);
};
const onClickB = useCallback(() => {
setDataB(o => o + 1);
}, []);
return <div>
<Cheap onClick={onClickA} name={`組件Cheap:${dataA}`}/>
<MemoExpensive onClick={onClickB} name={`組件Expensive:${dataB}`} />
</div>
}
複製代碼
memo
是 React v16.6.0 新增的方法,與 PureComponent 相似,前者負責 Function Component 的優化,後者負責 Class Component。它們都會對傳入組件的新舊數據進行淺比較,若是相同則不會觸發渲染。性能
因此useCallback
保證了onClickB
不發生變化,此時點擊Cheap
組件不會觸發Expensive
組件的刷新,只有點擊Expensive
組件纔會觸發。在實現減小沒必要要渲染的優化過程當中,useCallback
和memo
是一對利器。運行示例代碼優化
useCallback
源碼以下:
// 初始化階段
function mountCallback(callback, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
// 更新階段
function updateCallback(callback, deps) {
const hook = updateWorkInProgressHook();,
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
// 比較是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 若是相等,返回舊的 callback
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
複製代碼
核心邏輯就是比較deps
是否發生變化,若是有變化則返回新的callback
函數,不然返回原函數。其中比較方法areHookInputsEqual
內部實際調用了 React 的is
工具方法:
// 排除如下兩種特殊狀況:
// +0 === -0 // true
// NaN === NaN // false
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
);
}
複製代碼
useMemo
import React, { useMemo } from 'react';
function Comp() {
const v = 0;
const memoV = useMemo(() => v, []);
return <div>{memoV}</div>;
}
複製代碼
建立memoV
的開銷是沒有必要的,緣由與第一節提到的相同。
useMemo
的正確使用方法前文咱們提到,優化 React 組件性能的兩個主要思路之一是減小計算量,這也是useMemo
的用武之地:
import React, { useMemo } from 'react';
function Comp({ a, b }) {
const v = 0;
const calculate = (a, b) => {
// ... complex calculation
return c;
}
const memoV = useMemo((a, b) => v, [a, b]);
return <div>{memoV}</div>;
}
複製代碼
React Hook 對團隊的協做一致性要求很是高,useCallback
和useMemo
這一對方法就是很好的示例,更復雜的場景還有對useRef
、自定義 Hook 的使用等等。從經驗上來看,團隊在進行 Hook 編碼時須要特別增強 code review,不然很容易出現難以定位的 bug 或性能問題。當前 Hook 的各種方法還不完善,推特上爭論也不少,期待 React 後續版本提供出更成熟易用的方案。