整理自gitHub筆記html
useCallback
是解決函數組件過多內部函數致使的性能問題使用函數組件時常常定義一些內部函數,總以爲這會影響函數組件性能。也覺得useCallback
就是解決這個問題的,其實否則(Are Hooks slow because of creating functions in render?):react
得益於相對於 class 更輕量的函數組件,以及避免了 HOC, renderProps 等等額外層級,函數組件性能差不到那裏去;
useCallback
會形成額外的性能;deps
變化判斷。useCallback
其實也並非解決內部函數從新建立的問題。
仔細看看,其實無論是否使用useCallback
,都沒法避免從新建立內部函數:git
export default function Index() { const [clickCount, increaseCount] = useState(0); // 沒有使用`useCallback`,每次渲染都會從新建立內部函數 const handleClick = () => { console.log('handleClick'); increaseCount(clickCount + 1); } // 使用`useCallback`,但也每次渲染都會從新建立內部函數做爲`useCallback`的實參 const handleClick = useCallback(() => { console.log('handleClick'); increaseCount(clickCount + 1); }, []) return ( <div> <p>{clickCount}</p> <Button handleClick={handleClick}>Click</Button> </div> ) }
useCallback
解決的問題useCallback
實際上是利用memoize
減小沒必要要的子組件從新渲染github
import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( <button onClick={handleClick}>{children}</button> ) } const MemoizedButton = React.memo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); const handleClick = () => { console.log('handleClick'); increaseCount(clickCount + 1); } return ( <div> <p>{clickCount}</p> <MemoizedButton handleClick={handleClick}>Click</MemoizedButton> </div> ) }
即便使用了React.memo
修飾了Button
組件,可是每次點擊【Click】btn都會致使Button
組件從新渲染,由於:數組
Index
組件state發生變化,致使組件從新渲染;handleClick
,Button
也從新渲染。使用useCallback
優化:ide
import React, { useState, useCallback } from 'react' function Button(props) { const { handleClick, children } = props; console.log('Button -> render'); return ( <button onClick={handleClick}>{children}</button> ) } const MemoizedButton = React.memo(Button); export default function Index() { const [clickCount, increaseCount] = useState(0); // 這裏使用了`useCallback` const handleClick = useCallback(() => { console.log('handleClick'); increaseCount(clickCount + 1); }, []) return ( <div> <p>{clickCount}</p> <MemoizedButton handleClick={handleClick}>Click</MemoizedButton> </div> ) }
useCallback
的問題useCallback
的實參函數讀取的變量是變化的(通常來自state, props)export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // BUG:每次輸出都是初始值 }, []); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <p onClick={handleSubmit}>useCallback(fn, deps)</p> </> ) }
修改input
值,handleSubmit
處理函數的依舊輸出初始值。
若是useCallback
的實參函數讀取的變量是變化的,記得寫在依賴數組裏。函數
export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); // 每次輸出都是初始值 }, [text]); // 把`text`寫在依賴數組裏 return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <p onClick={handleSubmit}>useCallback(fn, deps)</p> </> ) }
雖然問題解決了,可是方案不是最好的,由於input輸入框變化太頻繁,useCallback
存在的意義沒啥必要了。性能
仍是上面例子,若是子組件比較耗時,問題就暴露了:優化
// 注意:ExpensiveTree 比較耗時記得使用`React.memo`優化下,要否則父組件優化也沒用 const ExpensiveTree = React.memo(function (props) { console.log('Render ExpensiveTree') const { onClick } = props; const dateBegin = Date.now(); // 很重的組件,不優化會死的那種,真的會死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return ( <div onClick={onClick}> <p>很重的組件,不優化會死的那種</p> </div> ) }); export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) }
問題:更新input值,發現比較卡頓。spa
useRef
解決方案 優化的思路:
ExpensiveTree
在無效的從新渲染,必須保證父組件re-render時handleSubmit
屬性值不變;handleSubmit
屬性值不變的狀況下,也要保證其可以訪問到最新的state。export default function Index() { const [text, updateText] = useState('Initial value'); const textRef = useRef(text); const handleSubmit = useCallback(() => { console.log(`Text: ${textRef.current}`); }, [textRef]); useEffect(() => { console.log('update text') textRef.current = text; }, [text]) return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) }
原理:
handleSubmit
由原來直接依賴text
變成了依賴textRef
,由於每次re-render時textRef
不變,因此handleSubmit
不變;text
更新時都更新textRef.current
。這樣雖然handleSubmit
不變,可是經過textRef
也是可以訪問最新的值。useRef
+useEffect
這種解決方式能夠造成一種固定的「模式」:export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useEffectCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( <> <input value={text} onChange={(e) => updateText(e.target.value)} /> <ExpensiveTree onClick={handleSubmit} /> </> ) } function useEffectCallback(fn, dependencies) { const ref = useRef(null); useEffect(() => { ref.current = fn; }, [fn, ...dependencies]) return useCallback(() => { ref.current && ref.current(); // 經過ref.current訪問最新的回調函數 }, [ref]) }
useRef
保持變化的值,useEffect
更新變化的值;useCallback
返回固定的callback。useReducer
解決方案const ExpensiveTreeDispatch = React.memo(function (props) { console.log('Render ExpensiveTree') const { dispatch } = props; const dateBegin = Date.now(); // 很重的組件,不優化會死的那種,真的會死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return ( <div onClick={() => { dispatch({type: 'log' })}}> <p>很重的組件,不優化會死的那種</p> </div> ) }); function reducer(state, action) { switch(action.type) { case 'update': return action.preload; case 'log': console.log(`Text: ${state}`); return state; } } export default function Index() { const [text, dispatch] = useReducer(reducer, 'Initial value'); return ( <> <input value={text} onChange={(e) => dispatch({ type: 'update', preload: e.target.value })} /> <ExpensiveTreeDispatch dispatch={dispatch} /> </> ) }
原理:
dispatch
自帶memoize
, re-render時不會發生變化;reducer
函數裏能夠獲取最新的state
。React官方推薦使用context
方式代替經過props
傳遞callback方式。上例改用context
傳遞callback
函數:
function reducer(state, action) { switch(action.type) { case 'update': return action.preload; case 'log': console.log(`Text: ${state}`); return state; } } const TextUpdateDispatch = React.createContext(null); export default function Index() { const [text, dispatch] = useReducer(reducer, 'Initial value'); return ( <TextUpdateDispatch.Provider value={dispatch}> <input value={text} onChange={(e) => dispatch({ type: 'update', preload: e.target.value })} /> <ExpensiveTreeDispatchContext dispatch={dispatch} /> </TextUpdateDispatch.Provider> ) } const ExpensiveTreeDispatchContext = React.memo(function (props) { console.log('Render ExpensiveTree') // 從`context`獲取`dispatch` const dispatch = useContext(TextUpdateDispatch); const dateBegin = Date.now(); // 很重的組件,不優化會死的那種,真的會死人 while(Date.now() - dateBegin < 600) {} useEffect(() => { console.log('Render ExpensiveTree --- DONE') }) return ( <div onClick={() => { dispatch({type: 'log' })}}> <p>很重的組件,不優化會死的那種</p> </div> ) });