歡迎關注我的公衆號睿Talk
,獲取我最新的文章:
一、前言
對於新手來說,沒寫過幾次死循環的代碼都不好意思說自己用過 React Hooks。本文將以useCallback
爲切入點,談談幾個 hook 的使用場景,以及性能優化的一些思考。
二、useCallback 使用場景
先看一個最簡單的例子:
// 用於記錄 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
- 3 -> 5 是一個死循環
如果明確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
的引用永遠不會變,因爲他它的依賴列表是空。可以根據實際情況將依賴加進去,就能確保依賴不變的情況下,函數的引用保持不變。
三、useCallback 依賴 state
假如在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
產生的開銷,性能怎麼可能會更好?
真正有助於性能改善的,是需要比較引用的場景,如上文提到的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
並不是提高性能的銀彈,錯誤的使用反而會適得其反。