歡迎關注個人公衆號睿Talk
,獲取我最新的文章:javascript
對於新手來講,沒寫過幾回死循環的代碼都很差意思說本身用過 React Hooks。本文將以useCallback
爲切入點,談談幾個 hook 的使用場景,以及性能優化的一些思考。java
這算是 Hooks 系列的第 3 篇,以前 2 篇的傳送門:
React Hooks 解析(上):基礎
React Hooks 解析(下):進階react
先看一個最簡單的例子:segmentfault
// 用於記錄 getData 調用次數 let count = 0; function App() { const [val, setVal] = useState(""); function getData() { setTimeout(()=>{ setVal('new data '+count); count++; }, 500) } useEffect(()=>{ getData(); }, []); return ( <div>{val}</div> ); }
getData
模擬發起網絡請求。在這種場景下,沒有useCallback
什麼事,組件自己是高內聚的。性能優化
若是涉及到組件通信,狀況就不同了:網絡
// 用於記錄 getData 調用次數 let count = 0; function App() { const [val, setVal] = useState(""); function getData() { setTimeout(() => { setVal("new data " + count); count++; }, 500); } return <Child val={val} getData={getData} />; } function Child({val, getData}) { useEffect(() => { getData(); }, [getData]); return <div>{val}</div>; }
就這麼輕輕鬆鬆,一個死循環就誕生了...閉包
先來分析下這段代碼的用意,Child
組件是一個純展現型組件,其業務邏輯都是經過外部傳進來的,這種場景在實際開發中很常見。函數
再分析下代碼的執行過程:性能
App
渲染Child
,將val
和getData
傳進去Child
使用useEffect
獲取數據。由於對getData
有依賴,因而將其加入依賴列表getData
執行時,調用setVal
,致使App
從新渲染App
從新渲染時生成新的getData
方法,傳給Child
Child
發現getData
的引用變了,又會執行getData
若是明確getData
只會執行一次,最簡單的方式固然是將其從依賴列表中刪除。但若是裝了 hook 的lint 插件,會提示:React Hook useEffect has a missing dependency
測試
useEffect(() => { getData(); }, []);
實際狀況極可能是當getData
改變的時候,是須要從新獲取數據的。這時就須要經過useCallback
來將引用固定住:
const getData = useCallback(() => { setTimeout(() => { setVal("new data " + count); count++; }, 500); }, []);
上面例子中getData
的引用永遠不會變,由於他它的依賴列表是空。能夠根據實際狀況將依賴加進去,就能確保依賴不變的狀況下,函數的引用保持不變。
假如在getData
中須要用到val
( useState 中的值),就須要將其加入依賴列表,這樣的話又會致使每次getData
的引用都不同,死循環又出現了...
const getData = useCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);
若是咱們但願不管val
怎麼變,getData
的引用都保持不變,同時又能取到val
最新的值,能夠經過自定義 hook 實現。注意這裏不能簡單的把val
從依賴列表中去掉,不然getData
中的val
永遠都只會是初始值(閉包原理)。
function useRefCallback(fn, dependencies) { const ref = useRef(fn); useEffect(() => { ref.current = fn; }, [fn, ...dependencies]); return useCallback(() => { const fn = ref.current; return fn(); }, [ref]); }
使用:
const getData = useRefCallback(() => { console.log(val); setTimeout(() => { setVal("new data " + count); count++; }, 500); }, [val]);
完整代碼能夠看這裏。
通常會以爲使用useCallback
的性能會比普通從新定義函數的性能好, 以下面例子:
function App() { const [val, setVal] = useState(""); const onChange = (evt) => { setVal(evt.target.value); }; return <input val={val} onChange={onChange} />; }
將onChange
改成:
const onChange = useCallback(evt => { setVal(evt.target.value); }, []);
實際性能會更差,能夠在這裏自行測試。究其緣由,上面的寫法幾乎等同於下面:
const temp = evt => { setVal(evt.target.value); }; const onChange = useCallback(temp, []);
能夠看到onChange
的定義是省不了的,並且額外還要加上調用useCallback
產生的開銷,性能怎麼可能會更好?
真正有助於性能改善的,有 2 種場景:
定義
時須要進行大量運算, 這種場景極少useEffect
,又或者是配合React.Memo
使用:const Child = React.memo(function({val, onChange}) { console.log('render...'); return <input value={val} onChange={onChange} />; }); function App() { const [val1, setVal1] = useState(''); const [val2, setVal2] = useState(''); const onChange1 = useCallback( evt => { setVal1(evt.target.value); }, []); const onChange2 = useCallback( evt => { setVal2(evt.target.value); }, []); return ( <> <Child val={val1} onChange={onChange1}/> <Child val={val2} onChange={onChange2}/> </> ); }
上面的例子中,若是不用useCallback
, 任何一個輸入框的變化都會致使另外一個輸入框從新渲染。代碼在這裏。
本文深刻講解了使用 hooks 過程當中死循環產生的緣由,並給出瞭解決方案。useCallback
並非提升性能的銀彈,錯誤的使用反而會拔苗助長。