这篇文章主要分享以下内容:什么是 debounce 函数?debounce 函数有什么用?如何写一个 debounce 函数?如何在 react 使用 debounce 函数?
我喜欢顾名思义,bounce 有跳动&反弹等意思,debounce 就是使之不跳动,debounce 函数可以理解为使一个函数不那么活跃,具体地说,就是减少一个函数的执行次数。
比如在一个 <input>
元素中绑定了一个叫 onInput
的事件,当我们把光标定位到输入框,每一次按下键盘,这个 onInput 事件都会被触发,事件对应的函数都会被执行一次。
事件函数每一次执行可能都要花费很长时间,例如根据你输入的内容在海量数据中进行查询,比如你要查询的是 'abc' 这个关键词,但事实上 onInput 事件会相继地被 'a','ab','abc' 这三个关键词所触发,而我们并不需要 'a' 和 'ab' 的查询结果,于是就产生了资源浪费,甚至可能会拖慢系统速度。
比如下面这个例子,每一次按下键盘都会触发 onInput 事件对应的函数:
再比如 <input type="range" onChange="onChangeRange"/>
,我们拖动滑块滑动的过程中 onChangeRange 函数会被不断触发,触发频率可能高达几十次/秒,如果 onChangeRange 函数运算复杂,就可能会影响系统的运行速度,如果我们不关心拖动滑块过程中的事件,只关心拖动结束后的事件,那就可以对 onChangeRange 进行 debounce,让它不那么活跃,减少它的执行次数。
debounce 的作用如下图所示,可以看到快速输入过程中事件函数中的关键运算并没有被执行,只有当停止输入 250ms 之后才执行。
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()
,是因为在 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 之后的事件函数。
在 react 中使用 debounce 的方法是:
所以从 bounce 切换到 debounce,基本不需要删除或更改已有的代码,主要是增加一个 debounce 函数。这个 debounce 函数可以自己定义,如上方例子,但建议用 lodash 版本,它可接收更多参数,有更多选项。
以下是 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()
这两个概念。