Learn Debounce and Throttle Through Animation

从动图轻松解题:防抖与节流

遇见问题痛点

用户与网页互动的方式有非常多种,像按钮、文本框、拖拉内容、画面滚动……等,监听过程中会反复触发相似的事件,因此需要特别留意用户的输入频率,过度频繁的触发事件将可能会导致:

  • 网页性能低下
  • 服务器负担加大

以实际案例来说,假设画面上有一个可输入的文本框,每次输入就会以文本框内的文字内容发送请求去索取第三方资源:

input.addEventListener('input', (e) => {
fetch(`/api/getOptions?query=${e.target.value}`);
});

了解问题痛点后,让我们来看看常见的解决方案 —— 防抖与节流。这两项方法综合了异步程序、闭包🔗其余参数🔗 等进阶概念,除了是实际常见的问题之外,面试时常会出现的题目。

防抖 Debounce

以日常生活为例:像是自动门,当有一群人排队等待进入门内时,并不会每有个人进入就执行一次「关门」的动作,而是每次有人进门就刷新关门的时间,直到没人进门时过一段时间就会自动关门,如此一来就节省了大量的「关门与开门的动作」。

实际一点的例子:举前面输入框搜索的例子,当用户输入时并不用急着马上提交请求,而是启动一个计时器,时间到了再提交最新的请求即可,如果在等待的过程中用户有输入的动作就重新计时。

防抖第一步:描述程序流程

撰写一个 debounce 函式,有两个参数分别是 等待被调用的回调函数等待时间 (默认 1000 毫秒),并事先创建 timeout 变量用于存储计时器,当被调用时:

  1. 停止当前执行中的计时器
  2. 启动新的计时器
  3. 当计时器完成计时,执行 回调函数 。

防抖第二步:创建函数与闭包

创造一个 debounce 函数,每当调用时就会返回一个匿名函数,并使用闭包 (Closure) 的特性让 timeout 变量可以与每个防抖函数共享。

function debounce(callback, delay = 1000) {
let timeout;
return (...args) => {};
}

防抖第三步:制作计时器

当每次调用 debounce 创建全新的计时器之前,如果有计时器就先删除上次的计时,并指定在指定延迟的时间之后调用回调函数。

function debounce(callback, delay = 250) {
let timeout;
return (...args) => {
if (timerId) {
clearTimeout(timerId);
}
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}

节流 Throttle

以日常生活为例:超级英雄变身有冷却时间,变身这个功能在启用过后一定时间才能再次使用,这期间无论如何尝试想要变身都无法成功。

实际一点的例子:当网页滑到底部时希望显示或加载一些新内容,因此使用节流来限制事件触发的数量。

节流第一步:描述程序流程

撰写一个 throttle 函式,两个参数分别是 等待被调用的回调函数等待时间 (默认 1000 毫秒),并事先:

  • 创建 throttleTimeout 变量存储节流计时器
  • 创建 callbackArgs 变量存储回调函数的参数

当闭包被执行时:

  • 将回调函数中的参数记录到 callbackArgs 中,才可以在最后一次输入时使用
  • 如果节流计时器存在,离开函数
  • 执行回调函数 (带入最新的参数)
  • 启动节流计时器于 throttleTimeout

当节流计时器结束时:

  • 取消 throttleTimeout 的内容,表示已无计时
  • 再次调用一次回调函数(带入最新的参数)

节流第二步:创建函数与闭包

创造一个 throttle 函数,每当调用时就会返回一个匿名函数,并使用闭包 (Closure) 的特性让 throttleTimeoutcallbackArgs 变量可以与每个节流函数共享。

function throttle(callback, delay = 1000) {
let throttleTimeout = null;
let callbackArgs = null;
return (...args) => {};
}

节流第三步:当闭包被执行时

callbackArgs = args;

首先存储最新的回调参数,这样才能在计时器结束时检查是否还有未执行的更新触发的回调函数存在,如果有就再执行一次整个回调函数。

再来当计时器不存在时就直接离开整个函数。

if (throttleTimeout) {
return;
}

紧接着执行一次回调函数的内容。

callback(...callbackArgs);

因为这项回调函数执行过了,所以取消闭包外的记录,改为 null (无)。

callbackArgs = null;

节流第三步:制作计时器

来到最重要的计时器部分:当计时器结束就取消其记录,检查是否还有未执行的回调参数存在,如果有就再执行一次整个回调函数。

throttleTimeout = setTimeout(() => {
throttleTimeout = null;
if (callbackArgs) {
callback(...callbackArgs);
}
}, delay);

以下是最终成果。

function throttle(callback, delay = 1000) {
// 创建一个闭包使以下变量可被所有节流事件共享
// 变量一:节流计时器
// 变量二:最近一次发生的事件
let throttleTimeout = null;
let callbackArgs = null;
// 返回一个匿名函数(回调事件处理),输入回调的参数
return (...args) => {
// 每次迭代中存储当下最新的事件
callbackArgs = args;
// 当节流计时器正在执行:离开函数
if (throttleTimeout) {
return;
}
// 当节流计时器没有在执行:执行回调函数并且启动节流计时器
// 执行回调函数(使用已记录的最新事件)
callback(...callbackArgs);
// 清空已记录的最新事件
callbackArgs = null;
// 创建一个新的节流计时器,记录于 throttleTimeout 中,在启动期间挡下传入的函数被执行
// 当计时结束,如果记录中有最新事件就再重新执行一次
throttleTimeout = setTimeout(() => {
// 当计时结束,清空 throttleTimeout 让传入的函数可被执行
throttleTimeout = null;
// 当计时器结束,检查是否还有未执行的事件,如果有就再执行一次整个回调函数
if (callbackArgs) {
callback(...callbackArgs);
}
}, delay);
};
}

总结

防抖整合许多动作为一;节流保持动作在一定的频率内。

实际范例可以参考看看我在六角学院 2022 秋季 JS 直播班所出的题目: 从动图轻松解题:防抖与节流

参考资料