logo科技微讯

debounce 函数有什么用?如何在 react 中使用 debounce?

作者:科技微讯
日期:2020-08-13
📜 文章

这篇文章主要分享以下内容:什么是 debounce 函数?debounce 函数有什么用?如何写一个 debounce 函数?如何在 react 使用 debounce 函数?

关于 debounce 函数

我喜欢顾名思义,bounce 有跳动&反弹等意思,debounce 就是使之不跳动,debounce 函数可以理解为使一个函数不那么活跃,具体地说,就是减少一个函数的执行次数。

比如在一个 <input> 元素中绑定了一个叫 onInput 的事件,当我们把光标定位到输入框,每一次按下键盘,这个 onInput 事件都会被触发,事件对应的函数都会被执行一次。

事件函数每一次执行可能都要花费很长时间,例如根据你输入的内容在海量数据中进行查询,比如你要查询的是 'abc' 这个关键词,但事实上 onInput 事件会相继地被 'a','ab','abc' 这三个关键词所触发,而我们并不需要 'a' 和 'ab' 的查询结果,于是就产生了资源浪费,甚至可能会拖慢系统速度。

比如下面这个例子,每一次按下键盘都会触发 onInput 事件对应的函数:

bounce

再比如 <input type="range" onChange="onChangeRange"/>,我们拖动滑块滑动的过程中 onChangeRange 函数会被不断触发,触发频率可能高达几十次/秒,如果 onChangeRange 函数运算复杂,就可能会影响系统的运行速度,如果我们不关心拖动滑块过程中的事件,只关心拖动结束后的事件,那就可以对 onChangeRange 进行 debounce,让它不那么活跃,减少它的执行次数。

debounce 的作用如下图所示,可以看到快速输入过程中事件函数中的关键运算并没有被执行,只有当停止输入 250ms 之后才执行。

debounce

如何写一个 debounce 函数

debounce 函数接收一个需要被 debounce 的事件函数以及一个时间作为参数,最后返回一个新的函数,其简单的写法可见下方代码。

var timeout = null 这行是关键,在上面这个例子中,用户每按一次键盘,都会产生一个新的 timeout ID,这个 ID 都保存在 timeout 这个变量中,旧的 ID 不断地被 clear,新的 ID 不断地被赋值给 timeout,当用户停止输入时,最新的 ID 才不会被 clear,于是就在一定时间之后执行事件函数。

function App() {
  // 这是 debounce 函数
  const debounce = function (f,wait) {
    var timeout = null;
    return function () {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        timeout = null;
        f.apply(this,arguments);
      },wait);
    };
  };
  // 这是事件函数,即需要被 debounce 的函数
  const inputHandler = function (e) {
    console.log(e.target.value);
  };
  // 通过 debounce 函数对事件函数进行 debounce
  const debounceInput = useCallback(debounce(inputHandler,250)[]);
  // react 需要这样把 e 传递给被 debounce 的事件函数,即传递给 debounceInput
  const onInput = (e) => {
    e.persist();
    debounceInput(e);
  };
  return (
    <div>
      <label htmlFor="input">输入文字:</label>
      <input id="input" type="text" onInput={onInput} />
    </div>
  );
}

关于 e.persist()

之所以要在事件函数中使用 e.persist(),是因为在 React 16 及之前的版本中 React 为了优化旧浏览器的性能,使用了一种名为 Event Pooling 的策略。在该策略下 React 的 SyntheticEvent 对象会被 pooled,即 SyntheticEvent 对象会在事件被触发后,使它的所有属性的值更改为 null,并在下一次监测到事件发生后重复利用该 SyntheticEvent 对象,这样可以降低旧浏览器的性能开销。

debounce 函数天生需要在一段时间后获取 SyntheticEvent 中的某个属性,但一段时间后 SyntheticEvent 每一个属性可能已经变成 null。顾名思义,e.persist() 的作用是 persist 这个 e,即保持住这个事件对象,让它不要把每个属性都变成 null,这样在 debounce 一段时间后依然可以获取里面的值。

e.persist() 只是解决这个问题的其中一种方法,另一种方法是触发事件函数的时候,不直接传递事件对象给事件函数,而是从该对象取出需要的值,把这个值传递给事件函数。

React 17 开始,React 完全取消了 Event Pooling 策略,所以可以直接把 e 传递给 debounce 之后的事件函数。

lodash.debounce

在 react 中使用 debounce 的方法是:

  • 按照以往的思路写 event handler;
  • 把这个 handler 传入 debounce 函数获得一个被 debounce 的函数;
  • 把 event 对象通过一个新的 event handler 传入被 debounce 的函数;

所以从 bounce 切换到 debounce,基本不需要删除或更改已有的代码,主要是增加一个 debounce 函数。这个 debounce 函数可以自己定义,如上方例子,但建议用 lodash 版本,它可接收更多参数,有更多选项。

use-debounce

以下是 2022-10-13 补充的内容。

现在不建议使用 lodash.debounce,建议使用 use-debounce,后者用起来更方便。

下面是 use-debounce 提供的一个例子,这个例子把事件对象的某个值传递给被 debounce 的事件函数,在 React 16 及之前的版本中,这样可以避免因为 Event Pooling 而导致事件函数无法获取事件对象中某个属性的问题,但在 React 17 及之后的版本中,已经无需这样做。

import { useDebouncedCallback } from "use-debounce";

function Input({ defaultValue }) {
  const [value, setValue] = useState(defaultValue);
  // Debounce callback
  const debounced = useDebouncedCallback(
    // function
    (value) => {
      setValue(value);
    },
    // delay in ms
    1000
  );

  // you should use `e => debounced(e.target.value)` as react works with synthetic events
  return (
    <div>
      <input
        defaultValue={defaultValue}
        onChange={(e) => debounced(e.target.value)}
      />
      <p>Debounced value: {value}</p>
    </div>
  );
}

总结

这篇文章介绍了 debounce 函数的作用和意义,并给出了一个简单的 debounce 函数实现方法,在实际项目中则可使用 use-debounce,文章顺便解释了 Event Pooling、e.persist() 这两个概念。

donation赞赏
thumbsup0
thumbsdown0
暂无评论