在編寫 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 體系下,優化思路主要有兩種:markdown
因此考察以下場景:閉包
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
是一對利器。運行示例代碼oop
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
,固然這種場景少之又少。
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 後續版本提供出更成熟易用的方案。