【react hook使用踩坑记录】react中使用useCallback来防抖节流

1,192 阅读3分钟

一、前言

最近在重温js的相关知识,写了一个小demo, 想实现一下防抖和节流,情景如下:

页面效果: 点击+1按钮实现counter + 1效果,也就是counter会从原本的0变成1,再点击, 1变成2, 以此类推;

image.png 最初的demo代码

import React, {useState} from 'react';

export default function App1() {
  const [counter, setCounter] = useState(0);

  // +1 操作
  const add = () => {
    setCounter(counter + 1);
  }

  // 重置操作
  const onReset = () => {
    setCounter(0);
  }

  return (
    <div>
      <h1>counter: {counter}</h1>
      <button onClick={add}>+1</button>
    </div>
  )
}

二、增加节流效果

既然想练习防抖和节流,那肯定是需要想让他的+1效果实现防抖和节流效果啦; 我们先使用lodash里面的throttle来实现节流,防抖(debounce)的使用和throttle的类似(我说的是使用,不是使用之后的最终效果哦),所以这里只写讲throttle;现在想给他增加的节流效果是:1秒内无论点击多少次,都只执行最后一次,代码如下:

    import React, {useState} from 'react';
 +  import _ from 'lodash';

    export default function App1() {
      const [counter, setCounter] = useState(0);

      const add = () => {
        setCounter(c => c + 1);
      }

      const onReset = () => {
        setCounter(0);
      }

  +   const throttle1 = _.throttle(add, 1e3);

      return (
        <div>
          <h1>counter: {counter}</h1>
          <button onClick={add}>+1</button>
          <button onClick={onReset}>重置</button>
   +      <hr/>
   +     <button onClick={throttle1}>节流1</button>
        </div>
      )
    }

但是实现了吗?并没有,和没加防抖是一样的效果 为什么呢?首先我们要知道throttle的实现原理,就是使用时间戳或者定时器来实现的节流。react重新渲染页面的标准是:状态更改。由于counter被更改了,所以App1这个组件重新渲染了,也就是重新执行了一遍,重新执行的话,之前throttle内部的时间戳或定时器被重新赋值,throttle也就失去了作用。

App1组件重新渲染不可以避免,但是我们可以避免throttle内部的时间戳或定时器被重新赋值,所以要怎么做呢?对throttle函数进行一个缓存,就可以保留之前throttle函数内部的状态,实现节流;就要用到useCallback来的对throttle函数进行缓存

useCallback(callback, deps): callback是回调函数, deps是依赖项,以数组形式存在,当deps中的依赖项变化的话,useCallback会被重新执行;

代码变成如下:

    import React, {useState, useCallback, useEffect} from 'react';
    import _ from 'lodash';

    export default function App1() {
      const [counter, setCounter] = useState(0);

      const add = () => {
        setCounter(counter + 1 );
      }

      const onReset = () => {
        setCounter(0);
      }

      const invalidThrottle1 = _.throttle(add, 1e3);

 +    const throttle2 = useCallback(_.throttle(add, 1e3), []);

 +    useEffect(() => {
        console.log('重新渲染', counter);
      })

      return (
        <div>
          <h1>counter: {counter}</h1>
          <button onClick={add}>+1</button>
          <button onClick={onReset}>重置</button>
          <hr/>
          <button onClick={invalidThrottle1}>无效节流1</button>
 +        <button onClick={throttle2}>节流2</button>
        </div>
      )
    }

效果如下: counter变成1之后就不再变化,无论怎么点击都不变化,所以节流实现了吗? image.png

事实上,节流实现了,为啥?我们来证明一下,给add函数增加一个打印,发现控制台的输出是节流输出的(如下图),但是为啥没有实现我们想要的效果:counter也节流增加呢?

  const add = () => {
    setCounter(counter + 1 ); // 节流了 但是没有得到最新值,页面一直是1
    console.log('throttle2', Date()); // 节流打印
  }

image.png

其实是因为这里形成了一个由counte变量和add函数组合而成的闭包,而这个闭包被useCallBack缓存下来,所以add函数里面的counter一直都是最开始的0, add执行以后就一直是1,所以页面不会有变化, 而要打破这个闭包,就需要获取到最新的counter,如何获取?

两种方式:

  1. 使用setState的函数形式 将add函数变成以下:
  const add = () => {
    setCounter(c => c + 1);
  }
  1. 配合ref使用
  const latestCounter = useRef(0);
  const add = () => {
    setCounter(latestCounter.current + 1)
  }

两种方式都可以得到以下的效果,也就是我们最先想要的效果

image.png

可能有的同学还会这样来使用useCallback

const invalidThrottle3 = useCallback(_.throttle(() => {
  add()
}, 1e3), [counter]);

因为依赖counter,add重新执行,也就获取到了最新的counter,但是这样useCallback来缓存函数的意义就没有了,节流防抖也就失效了

总结

这里的坑主要是闭包导致的状态不能更新,打破闭包的方式,就是获取最新的counter: 两种方式:

  1. 使用setState的函数形式
  2. 配合ref使用
  3. 注意useCallback依赖项的使用