案例
// 新建文章組件
function EditArticle() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [other, setOther] = useState("");
// 獲取當前「標題」和「內容」的長度
const getTextLen = () => {
return [title.length, content.length];
};
// 上報當前「標題」和「內容」的長度
const report = () => {
const [titleLen, contentLen] = getTextLen();
if (contentLen > 0) {
console.log(`埋點 >>> 標題長度 ${titleLen}, 內容長度 ${contentLen}`);
}
};
/**
* 副作用
* 當「標題」長度變化時,上報
*/
useEffect(() => {
report();
}, [title]);
return (
<div className="App">
文章標題 <input value={title} onChange={(e) => setTitle(e.target.value)} />
文章內容 <input value={content} onChange={(e) => setContent(e.target.value)} />
其他不相關狀態: <input value={other} onChange={(e) => setOther(e.target.value)} />
<MemoArticleTypeSetting getTextLen={getTextLen} />
</div>
);
}
enum ArticleType {
WEB = "前端",
SERVER = "后端",
}
// 子組件,修改文章類型(無需關注,它只是接受了父組件的一個參數而已)
const ArticleTypeSetting: FC<{ getTextLen: () => number[] }> = ({ getTextLen }) => {
console.log(" --- ArticleTypeSetting 組件重新渲染 --- ");
const [articleType, setArticleType] = useState<ArticleType>(ArticleType.WEB);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setArticleType(e.target.value as ArticleType);
console.log( "埋點 >>> 切換類型,當前「標題」和「內容」長度:", getTextLen() );
};
return (
<div>
<div>文章類型組件,當選擇類型時上報「標題」和「內容」長度</div>
<div>
{[ArticleType.WEB, ArticleType.SERVER].map((type) => (
<div>
<input type="radio" value={type} checked={articleType === type} onChange={handleChange} />
{type}
</div>
))}
</div>
</div>
);
};
const MemoArticleTypeSetting = memo(ArticleTypeSetting);
哪些地方需要優化
- 子組件 ArticleTypeSetting 是使用 memo 包裹的,這個組件是希望盡可能的減少渲染次數的(假裝這個組件有性能問題,一般不用包)。但是,現在每當修改任意一個值(如 other),子組件都會重新渲染,這顯然是沒有達到優化的預期的。
- 這里不規范, useEffect 中使用了 report 函數,但是沒有將它放到依賴數組中。
對代碼進行了一些修改
// 獲取當前「標題」和「內容」的長度
const getTextLen = useCallback(() => {
return [title.length, content.length];
}, [title, content]);
// 上報當前「標題」和「內容」的長度
const report = useCallback(() => {
const [titleLen, contentLen] = getTextLen();
if (contentLen > 0) {
console.log(`埋點 >>> 內容長度 ${titleLen}, 內容長度 ${contentLen}`);
}
}, [getTextLen]);
/**
* 副作用
* 當「標題」長度變化時,上報
*/
useEffect(() => {
report();
}, [title, report]);
還有問題
當 「文章內容」修改了之后,會觸發 useEffect 繼續上報
為什么
初衷只是使用 useCallback 避免頻繁調用,但當一個 useCallback 的依賴項變化后,這個 useEffect 會被執行,就像上面修改過后的代碼一樣,「文章內容」修改了之后,也會觸發 useEffect 的,這就是「useCallback 帶來的隱式依賴問題」。
如何解決
將 函數綁定到 useRef 上來解決
const getTextLenRef = useRef<() => [number, number]>(() => [0, 0]);
// 獲取當前「標題」和「內容」的長度
getTextLenRef.current = () => {
return [title.length, content.length];
};
// 上報當前「標題」和「內容」的長度
const report = () => {
const [titleLen, contentLen] = getTextLenRef.current();
if (contentLen > 0) {
console.log(`埋點 >>> 標題長度 ${titleLen}, 內容長度 ${contentLen}`);
}
};
/**
* 副作用
* 當「標題」長度變化時,上報
*/
useEffect(() => {
report();
}, [title]);
將函數綁定到 Ref上,ref 引用不論怎樣都保持不變,而且函數每次 render ref 上又會綁定最新的函數,不會有閉包問題。我在開發一個復雜項目中,大量的使用了這種方式,這讓我的開發效率提升。它讓我專注於寫業務,而不是專注於解決閉包問題。
思考我們最開始使用 useCallback 的理由
只有「需要保存一個函數閉包結果,如配合 debounce、throttle 使用」這個是真正需要使用 useCallback 的,其他的都可能帶來風險
在絕大多數情況下,開發者想要的僅僅只是避免函數的引用變化而已,而 useCallback 帶來的隱式依賴問題會給你帶來很大的麻煩,所以推薦使用 useRefCallback,把函數掛到 ref 上,這樣代碼更不容易留下隱患或帶來問題,還可以省去維護 useCallback 依賴項的精力
https://github.com/huyaocode/webKnowledge/issues/12
