在前端开发中,我们经常遇到这样的场景:用户在搜索框疯狂输入,或者疯狂滚动页面。如果我们不对这些行为加以限制,浏览器就会像得了“帕金森”一样,不仅控制台里 AJAX 请求满天飞,页面也可能因为频繁重绘而卡顿。
这时候,就需要请出闭包应用的两尊大神:防抖 (Debounce) 和 节流 (Throttle) 。
它们虽然是“亲兄弟”,也是性能优化的黄金搭档,但性格却截然不同。今天我们就结合实战代码,来彻底搞懂它们。
一、 为什么我们需要它们?
想象一下以下两个场景:
- 搜索建议 (Code Suggest / Baidu Ajax): 用户想搜 "JavaScript",每敲一个字母触发一次
keyup。如果没有限制,输入10个字符就会发10次请求。前9次都是没用的,既浪费带宽,又因为请求返回顺序的不确定性导致数据错乱。 - 页面滚动/拖拽: 用户滚动页面加载更多。
scroll事件触发频率极高(可能几毫秒一次)。如果在回调里做复杂计算或 DOM 操作,FPS 会瞬间掉底,用户体验极差。
核心问题: 事件触发频率太高,超过了浏览器的处理能力或业务需求。
解决方案: 利用 闭包 和 定时器,控制函数执行的频率。
二、 防抖 (Debounce):最后一次才是真爱
核心口诀: “管你触发多少次,我只认最后一次。”
1. 场景与比喻
生活中的例子: 就像坐电梯。
一个人进来了,电梯门开了。电梯准备关门(设置延时)。如果在关门前又进来一个人,电梯门得重新打开,倒计时重新开始。只有当没人再进来(一定时间内无操作),电梯门才会真正关上运行。
技术场景:
- 搜索框输入(用户输完才发请求)。
- 窗口大小调整(
resize),调整完窗口后再计算布局。
2. 代码实现
防抖的精髓在于:每次触发事件,都清除上一次的定时器,重新开始倒计时。
JavaScript
// 防抖函数:闭包的高阶应用
function debounce(fn, delay) {
// 1. 创建一个变量用来保存定时器ID
// 这个变量存在于闭包中,不会被垃圾回收,所有的调用共享这一个 timer
let timer = null;
return function() {
// 保存当前的上下文 this 和参数 args
// 否则在 setTimeout 中 this 会指向 window
let that = this;
let args = arguments;
// 2. 如果之前已经有定时器了,说明上一次操作还没结束,赶紧清除掉!
if (timer) clearTimeout(timer);
// 3. 开启一个新的定时器,重新倒计时
timer = setTimeout(function() {
fn.apply(that, args); // 执行真正的业务逻辑
}, delay);
}
}
3. 效果
用户一直按键盘,timer 一直被 clear 也就是一直被推迟。只有用户停手 delay 毫秒后,函数才会执行。
三、 节流 (Throttle):天下武功,唯快不破(但有冷却时间)
核心口诀: “在该段时间内,无论你怎么点,我也只执行一次。”
1. 场景与比喻
生活中的例子: FPS 游戏的射速。
即使你拿着鼠标疯狂连点(触发事件),AK-47 的射速(执行频率)也是固定的。子弹射出后需要“冷却时间” (CD),CD 转好之前,你点烂鼠标也射不出子弹。
技术场景:
- 滚动加载(Scroll Loading):不需要每像素都检查,每隔 200ms 检查一次滚动位置即可。
- 高频点击提交:防止用户疯狂点击按钮提交表单。
2. 代码实现
节流的精髓在于:利用时间戳或定时器,判断当前时间是否已经超过了规定的“冷却时间”。
这里我们采用一个综合版(结合了时间戳和定时器),保证首发响应快,结尾也能补一次。
JavaScript
// 节流函数:FPS 游戏射速控制器
function throttle(fn, delay) {
let last = 0; // 上次触发的时间戳
let deferTimer = null; // 用于处理最后一次的定时器
return function() {
let that = this;
let args = arguments;
let now = +new Date(); // 获取当前毫秒数
// 判断:距离上次执行是否超过了 delay (CD是否转好了?)
if (last && now < last + delay) {
// 情况A: 还没到时间 (CD中)
// 设置一个定时器,保证如果用户停止操作,最后一次也能被执行
clearTimeout(deferTimer);
deferTimer = setTimeout(function() {
last = now;
fn.apply(that, args);
}, delay);
} else {
// 情况B: 时间到了 (CD好了) 或者 第一次触发
last = now; // 更新上次执行时间
fn.apply(that, args); // 立即开火!
}
}
}
四、 深度总结:防抖 vs 节流
| 特性 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 核心逻辑 | 延时执行。只要你一直动,我就一直不执行。 | 间隔执行。无论你动多快,我按自己的节奏来。 |
| 比喻 | 电梯关门 / 英雄回城 (被打断就重来) | 游戏射速 / 技能冷却 (CD) |
| 最佳场景 | 搜索框输入、文本自动保存 | 页面滚动、窗口调整、鼠标移动事件 |
| 闭包作用 | 存储 timer ID,确保能清除上一次 | 存储 last 时间戳,确保能计算时间差 |
闭包在这里扮演了什么角色?
在 debounce 和 throttle 函数中,变量 timer 和 last 都是定义在外部函数中的。
当返回的内部函数被执行时,它们依然能访问这些变量。这就是闭包。
如果没有闭包,我们就需要把 timer 定义在全局变量里,这会污染全局作用域,而且如果有两个输入框都需要防抖,全局变量就会冲突。闭包完美解决了这个问题,让每个防抖函数都有自己独立的“私有变量”。
五、 实战演示
我们将两种策略挂载到 keyup 事件上对比一下:
- 普通 Ajax:输入
hello,控制台打印 5 次。 - 防抖 Ajax:输入
hello,中间不打印,停手后打印 1 次(最后一次)。 - 节流 Ajax:输入
hellooooooooo...此时用户一直没停手,但控制台会每隔 500ms 规律地打印一次。
结语
- 防抖 是为了防止“误触”和“过度敏感”,它关注结果。
- 节流 是为了“降频”和“减轻压力”,它关注过程。
掌握了这两个高阶函数,你的代码性能和用户体验都将上一个台阶。下次面试官问起闭包的应用场景,别再只说“变量不销毁”了,直接把这两个“守门员”甩出来!