你不知道的 useCallback

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

對於新手來講,沒寫過幾回死循環的代碼都很差意思說本身用過 React Hooks。本文將以useCallback爲切入點,談談幾個 hook 的使用場景,以及性能優化的一些思考。java

這算是 Hooks 系列的第 3 篇,以前 2 篇的傳送門:
React Hooks 解析(上):基礎
React Hooks 解析(下):進階react

2、useCallback 使用場景

先看一個最簡單的例子: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組件是一個純展現型組件,其業務邏輯都是經過外部傳進來的,這種場景在實際開發中很常見。函數

再分析下代碼的執行過程:性能

  1. App渲染Child,將valgetData傳進去
  2. Child使用useEffect獲取數據。由於對getData有依賴,因而將其加入依賴列表
  3. getData執行時,調用setVal,致使App從新渲染
  4. App從新渲染時生成新的getData方法,傳給Child
  5. Child發現getData的引用變了,又會執行getData
  6. 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的引用永遠不會變,由於他它的依賴列表是空。能夠根據實際狀況將依賴加進去,就能確保依賴不變的狀況下,函數的引用保持不變。

3、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]);

完整代碼能夠看這裏

4、性能

通常會以爲使用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, 任何一個輸入框的變化都會致使另外一個輸入框從新渲染。代碼在這裏

5、總結

本文深刻講解了使用 hooks 過程當中死循環產生的緣由,並給出瞭解決方案。useCallback並非提升性能的銀彈,錯誤的使用反而會拔苗助長。

相關文章
相關標籤/搜索