class 組件中性能優化能夠經過 shouldComponentUpdate
實現或者繼承自 PureComponent
,固然後者也是經過 shouldComponentUpdate
去作的,內部對 state
和 props
進行了 shallowEqual。數組
對於函數組件來講並無這個生命週期能夠調用,所以想實現性能優化只能經過 React.memo(<Component />)
來作,這種作法和繼承 PureComponent
的原理一致。性能優化
另外若是你的函數組件須要拿到它的 ref,可使用如下工具函數:微信
function memoForwardRef<N, P>(comp: RefForwardingComponent<N, P>) {
return memo(forwardRef<N, P>(comp));
}
複製代碼
可是並非以上作法之後性能就萬事大吉了,你還得保證傳遞的 props
以及內部的狀態的引用不發生預期以外的變化。閉包
對於函數組件來講,變量的引用是須要重點關注的問題,不管是函數亦或者對象。函數
const Child = React.memo(({ columns }) => {
return <Table columns={columns} />
})
const Parent = () => {
const data = [];
return <Child columns={data} />
}
複製代碼
對於以上組件來講,每次 Parent
渲染的時候雖然 columns
內容沒有變,可是 columns
的引用已經變了。當 props
傳遞給 Child
的時候,即便使用了 React.memo
可是性能優化也失效了。工具
對於這種狀況,能夠經過 useMemo
將引用存儲起來,依賴不變引用也就不變。性能
const data = useMemo(() => [], [])
複製代碼
useMemo
的場景可能是用於值的計算。好比密集型計算場景下你確定不但願組件從新渲染的時候,依賴項沒有變動缺重複執行計算函數獲得相同的值。優化
對於函數來講,若是你想保存它的引用的話可使用 useCallback
來作。ui
function Counter() {
const [count, setCount] = useState(0)
// 這樣寫函數,每次從新渲染都會再次建立一個新的函數
const onIncrement = () => {
setCount(count => count + 1)
}
const onIncrement = useCallback(() => {
setCount(count => count + 1)
}, [])
return (
<div> <button onClick={onIncrement}>INCREMENT</button> <p>{count}</p> </div>
)
}
複製代碼
對於以上代碼來講,組件每次渲染的時候使用了 useCallback
包裹的 onIncrement
函數引用不會改變,這也就意味着不須要頻繁建立及銷燬函數了。spa
可是在 useCallback
存在依賴的狀況下函數引用並不必定按照你的想法正常保持不變,好比以下案例:
function Counter() {
const [count, setCount] = useState(0)
const onIncrement = useCallback(() => {
setCount(count => count + 1)
}, [])
const onLog = useCallback(() => {
console.log(count)
}, [count])
return (
<div> <button onClick={onIncrement}>INCREMENT</button> <button onClick={onLog}>Log</button> <p>{count}</p> </div>
)
}
複製代碼
當 count
每次改變形成組件從新渲染的時候,onLog
函數都會從新建立一次。兩種常規方法能夠保持在這種狀況下函數引用不被改變。
useEventCallback
useReducer
function useEventCallback(fn, dependencies) {
const ref = useRef(() => {
throw new Error('Cannot call an event handler while rendering.');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
複製代碼
useEventCallback
使用了 ref
不變的特性,保證回調函數的引用永遠不變。另外在 Hooks 中,dispatch
也是不變的,因此把依賴 ref
改爲 dispatch
,而後在回調中調用 dispatch
就是另外一種作法了。
凡事都有兩面性,在引入以上這些性能優化的時候你已經下降了本來的性能,畢竟它們都是有使用代價的,咱們能夠來閱讀下 useCallback
及 useMemo
的核心源碼:
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)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateMemo(nextCreate, 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)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
複製代碼
上述源碼實現思路大體是從 fiber 中取出 memoizedState
,而後對比先後 Deps,對比的實現也採用了 shallowEqual,最後若是有變化的話就重置 memoizedState
。
能夠看出來,本文中講到的性能優化方案基本都是採用了 shallowEqual 來對比先後差別,因此不必爲了性能優化而優化。
Hooks 的坑 99% 都是閉包引發的,咱們經過一個例子來了解下什麼狀況下會由於閉包致使問題。
function App() {
const [state, setState] = React.useState(0)
// 連點三次你以爲答案會是什麼?
const handleClick = () => {
setState(state + 1)
setTimeout(() => {
console.log(state)
}, 2000)
}
return (
<>
<div>{state}</div>
<button onClick={handleClick} />
</>
)
}
複製代碼
上述代碼觸發三次 handleClick
後你以爲答案會是什麼?可能答案與你所想的不大同樣,結果是:
0 1 2
由於每次 render 都有一份新的狀態,所以上述代碼中的 setTimeout
使用產生了一個閉包,捕獲了每次 render 後的 state
,也就致使了輸出了 0、一、2。
若是你但願輸出的內容是最新的 state
的話,能夠經過 useRef
來保存 state
。前文講過 ref
在組件中只存在一份,不管什麼時候使用它的引用都不會產生變化,所以能夠來解決閉包引起的問題。
function App() {
const [state, setState] = React.useState(0)
// 用 ref 存一下
const currentState = React.useRef(state)
// 每次渲染後更新下值
useEffect(() => {
currentState.current = state
})
const handleClick = () => {
setState(state + 1)
// 這樣定時器裏經過 ref 拿到最新值
setTimeout(() => {
console.log(currentState.current)
}, 2000)
}
return (
<>
<div>{state}</div>
<button onClick={handleClick} />
</>
)
}
複製代碼
其實閉包引起的問題多半是保存了 old 的值,只要想辦法拿到最新的值其實基本上就解決問題了。
若是你以爲我有遺漏什麼或者寫的不對的,歡迎指出。
我很想聽聽你的想法,謝謝閱讀。
微信掃碼關注公衆號,訂閱更多精彩內容 | 加筆者微信羣聊技術 |
---|---|