React hook之useCallback

useCallback的作用

Pass an inline callback and an array of dependencies. useCallback will
return a memoized version of the callback that only changes if one of
the dependencies has changed.

其实就是返回一个函数,只有当依赖项发生变化时才更新

代码案例

代码案例来源于 掘金作者:yi个程序猿
链接:https://juejin.cn/post/6844904101445124110

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function useCallbackDemo() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [count3, setCount3] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
      <div>
        <Button
          onClickButton={() => {
            setCount3(count3 + 1);
          }}
        >
          Button3
        </Button>
      </div>
    </div>
  );
}
import React from 'react';

const Button = ({ onClickButton, children }) => {
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);

上述案例中只有Button2点击时自身数值才会变化,点击Button1和Button3时Button2后面的数字不会变化
发生上述现象的原因其实就是useCallback和React.memo。
React.memo会对props做比较,如果props没有变化则不会重新渲染该组件,而useCallback则根据依赖项对需要变化的动作进行了缓存,依赖项没有变化点击事件便不会返回新的函数,props便没有变化,于是子组件不会被重新渲染,而当依赖变化时,此时的点击事件是返回一个新的函数,对子组件来说,props发生了变化,于是重新进行渲染。

function A() {
  // ...
  const cb = () => {}/* 创建了 */;
}

function B() {
  // ...
  const cb = React.useCallback(() => {}/* 还是创建了 */, [a, b]);
}
const a = () => {};
const b = () => {};
a === b; // false

刚才的代码1中可以发现,无论是否使用useCallback,函数都会被创建,所以使用useCallback不是在于函数是否被创建或者返回,而是是否返回新的函数

回头再看上面的 Button 组件都需要一个 onClickButton 的 props ,尽管组件内部有用 React.memo 来做优化,但是我们声明的 handleClickButton1 是直接定义了一个方法,这也就导致只要是父组件重新渲染(状态或者props更新)就会导致这里声明出一个新的方法,新的方法和旧的方法尽管长的一样,但是依旧是两个不同的对象(看上述a和b两个方法),React.memo 对比后发现对象 props 改变,就重新渲染了

const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count2]);

上面最开始的案例中Button2的方法我们用useCallback进行了包裹,并传入了count2依赖项,这里 useCallback 就会根据 count2 是否发生变化,从而决定是否返回一个新的函数,函数内部作用域也随之更新。
由于我们的这个方法只依赖了 count2 这个变量,而且 count2 只在点击 Button2 后才会更新 handleClickButton2,所以就导致了我们点击 Button1 不重新渲染 Button2 的内容。

使用useCallback的问题

  1. 不更新导致的问题
const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, []);

如果我们将Button2的点击事件中传入的依赖项改为空数组,这也就意味着这个方法没有依赖值,将不会被更新。且由于 JS 的静态作用域导致此函数内 count2 永远都 0。
可以点击多次 Button2 查看变化,会发现 Button2 后面的值只会改变一次。因为上述函数内的 count2 永远都是 0,就意味着每次都是 0 + 1,Button 所接受的 count props,也只会从 0 变成 1且一直都将是 1,而且 handleClickButton2 也因没有依赖项不会返回新的方法,就导致 Button 组件只会因 count 改变而更新一次。

  1. 频繁更新导致的问题
    如果一个 callback 依赖于一个经常变化的 state,这个 callback 的引用是无法缓存的。
    例如react文档中提出的问题:
 function Form() {
  const [text, updateText] = useState('');

  const handleSubmit = useCallback(() => {
    console.log(text);
  }, [text]); // 每次 text 变化时 handleSubmit 都会变

  return (
    <>
      <input value={text} onChange={(e) => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} /> // 很重的组件,不优化会死的那种
    </>
  );
}

这个问题无解的原因在于,在 input 的使用中 text 的变化肯定是相当频繁的,callback 内部对 state 的访问依赖于 JavaScript 函数的闭包。如果希望 callback 不变,那么访问的之前那个 callback 函数闭包中的 state 会永远是当时的值。

react文档中的答案:

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

  useLayoutEffect(() => {
    textRef.current = text; // 将 text 写入到 ref
  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // 从 ref 中读取 text
    alert(currentText);
  }, [textRef]); // handleSubmit 只会依赖 textRef 的变化。不会在 text 改变时更新

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

文档里给出的解法乍一看可能不太好理解,我们一步步慢慢来。首先,因为在函数式组件里没有了 this 来存放一些实例的变量,所以 React 建议使用 useRef 来存放一些会发生变化的值,useRef 并不再单单是为了 DOM 的 ref 准备的,同时也会用来存放组件实例的属性。在 updateText 完成对 text 的更新后,再在 useLayoutEffect (等效于 didMount 和 didUpdate) 里写入 textRef.current 中。这样,在 handleSubmit 里取出的 textRef 中存放的值就永远是新值了。

合理使用useCallback

不要把所有方法都套上useCallback

const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

const handleClickButton1 = () => {
  setCount1(count1 + 1)
};
const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1)
}, [count2]);

return (
  <>
    <button onClick={handleClickButton1}>button1</button>
    <button onClick={handleClickButton2}>button2</button>
  </>
)

上面这种写法在当前组件重新渲染时会声明一个新的 handleClickButton1 函数,下面 useCallback 里面的函数也会声明一个新的函数,被传入到 useCallback 中,尽管这个函数有可能因为 inputs 没有发生改变不会被返回到 handleClickButton2 变量上。
那么在我们这种情况它返回新的函数和老的函数也都一样,因为下面 已经都会被渲染一下,反而使用 useCallback 后每次执行到这里内部要要比对 inputs 是否变化,还有存一下之前的函数,消耗更大了。

那么怎么用呢?
useCallback 是要配合子组件的 shouldComponentUpdate 或者 React.memo 一起来使用的,否则就是反向优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值