前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你不知道的 useCallback

你不知道的 useCallback

作者头像
Dickensl
发布2022-06-14 13:47:04
7080
发布2022-06-14 13:47:04
举报
文章被收录于专栏:睿Talks

一、前言

对于新手来说,没写过几次死循环的代码都不好意思说自己用过 React Hooks。本文将以useCallback为切入点,谈谈几个 hook 的使用场景,以及性能优化的一些思考。

这算是 Hooks 系列的第 3 篇,之前 2 篇的传送门:

React Hooks 解析(上):基础

React Hooks 解析(下):进阶

二、useCallback 使用场景

先看一个最简单的例子:

代码语言:javascript
复制
// 用于记录 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什么事,组件本身是高内聚的。

如果涉及到组件通讯,情况就不一样了:

代码语言:javascript
复制
// 用于记录 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

代码语言:javascript
复制
useEffect(() => {
  getData();
}, []);

实际情况很可能是当getData改变的时候,是需要重新获取数据的。这时就需要通过useCallback来将引用固定住:

代码语言:javascript
复制
const getData = useCallback(() => {
  setTimeout(() => {
    setVal("new data " + count);
    count++;
  }, 500);
}, []);

上面例子中getData的引用永远不会变,因为他它的依赖列表是空。可以根据实际情况将依赖加进去,就能确保依赖不变的情况下,函数的引用保持不变。

三、useCallback 依赖 state

假如在getData中需要用到val( useState 中的值),就需要将其加入依赖列表,这样的话又会导致每次getData的引用都不一样,死循环又出现了...

代码语言:javascript
复制
const getData = useCallback(() => {
  console.log(val);

  setTimeout(() => {
    setVal("new data " + count);
    count++;
  }, 500);
}, [val]);

如果我们希望无论val怎么变,getData的引用都保持不变,同时又能取到val最新的值,可以通过自定义 hook 实现。注意这里不能简单的把val从依赖列表中去掉,否则getData中的val永远都只会是初始值(闭包原理)。

代码语言:javascript
复制
function useRefCallback(fn, dependencies) {
  const ref = useRef(fn);

  // 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
  // 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn
  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

使用:

代码语言:javascript
复制
const getData = useRefCallback(() => {
  console.log(val);

  setTimeout(() => {
    setVal("new data " + count);
    count++;
  }, 500);
}, [val]);

完整代码可以看这里

四、性能

一般会觉得使用useCallback的性能会比普通重新定义函数的性能好, 如下面例子:

代码语言:javascript
复制
function App() {
  const [val, setVal] = useState("");

  const onChange = (evt) => {
    setVal(evt.target.value);
  };

  return <input val={val} onChange={onChange} />;
}

onChange改为:

代码语言:javascript
复制
const onChange = useCallback(evt => {
  setVal(evt.target.value);
}, []);

实际性能会更差,可以在这里自行测试。究其原因,上面的写法几乎等同于下面:

代码语言:javascript
复制
const temp = evt => {
  setVal(evt.target.value);
};

const onChange = useCallback(temp, []);

可以看到onChange的定义是省不了的,而且额外还要加上调用useCallback产生的开销,性能怎么可能会更好?

真正有助于性能改善的,有 2 种场景:

  • 函数定义时需要进行大量运算, 这种场景极少
  • 需要比较引用的场景,如上文提到的useEffect,又或者是配合React.Memo使用:
代码语言:javascript
复制
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并不是提高性能的银弹,错误的使用反而会适得其反。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、useCallback 使用场景
  • 三、useCallback 依赖 state
  • 四、性能
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档