在前端开发中,resize、scroll、mousemove 等事件触发频率极高。如果不加限制,会引发严重的性能问题(如页面卡顿、服务器压力过大)。
防抖 和节流就是为了解决这个问题而生的两兄弟,虽然目的相同(减少执行频率),但策略完全不同。
1. 核心概念与区别
用一个生动的例子来区分它们:
| 维度 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 核心思想 | 最后一个人说了算 | 按照时间表办事 |
| 生活比喻 | 坐电梯:只要有人进来,电梯就重新计时,直到没人进来了才关门运行。 | 公交车:不管人多拥挤,每隔 10 分钟发一班车。 |
| 执行规律 | 狂点 100 次,可能只执行 1 次(最后一次)。 | 狂点 100 次,可能执行 10 次(均匀分布)。 |
| 适用场景 | 只需要最终结果(如搜索框输入)。 | 需要持续的过程反馈(如滚动加载)。 |
2. 防抖 (Debounce)
2.1 原理
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则打断之前的计划,重新计时。
2.2 应用场景
- 搜索框输入 (Input Search):用户停止输入 500ms 后才发送请求,避免输一个字请求一次。
- 窗口调整 (Window Resize):调整大小时不计算,调整完毕后才重新计算布局。
- 文本编辑器保存:用户停笔后自动保存。
2.3 手写源码 (带 immediate 参数)
面试难点:如何实现"立即执行一次,然后开始防抖"?(比如点赞按钮,想要立刻变色,但防止后续重复点击)。
javascript
/**
* 防抖函数
* @param {Function} func 需要执行的函数
* @param {number} wait 等待时间 (ms)
* @param {boolean} immediate 是否立即执行 (true: 立即执行, false: 延迟执行)
*/
function debounce(func, wait, immediate = false) {
let timeout;
return function (...args) {
// 保存当前的 this 和 arguments,确保 func 执行时上下文正确
const context = this;
// 核心逻辑:如果在 wait 时间内再次触发,清除上一次的定时器
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果 timeout 为 null,说明是第一次触发(或者上一个周期已经结束)
const callNow = !timeout;
// 设置定时器,wait 毫秒后将 timeout 置空,代表这一轮防抖结束
// 注意:这里的 setTimeout 不执行 func,只负责重置状态
timeout = setTimeout(() => {
timeout = null;
}, wait);
// 只有符合条件才立即执行
if (callNow) func.apply(context, args);
} else {
// 普通防抖:延迟执行
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
};
}
3. 节流 (Throttle)
3.1 原理
规定在一个单位时间内,只能触发一次函数。稀释函数的执行频率。
3.2 应用场景
- 滚动加载 (Scroll):监听滚动条位置,每隔 200ms 计算一次是否到底部。
- DOM 元素拖拽 (Drag) :
mousemove事件高频触发,每隔 50ms 更新一次位置即可。 - 按钮连击:游戏中限制发射子弹的频率(攻速限制)。
3.3 手写源码 (时间戳 + 定时器版)
节流的实现通常有两种流派:
- 时间戳版:触发立即执行,最后一次停止触发后不再执行。(首触发)
- 定时器版:触发不立即执行,最后一次停止触发后还会再执行一次。(尾触发)
下面我们将两者结合,或者通过 immediate 参数控制。为了面试清晰,这里提供一个逻辑最清晰的切换版。
javascript
/**
* 节流函数
* @param {Function} func 需要执行的函数
* @param {number} wait 间隔时间 (ms)
* @param {boolean} immediate 是否立即执行 (true: 时间戳版, false: 定时器版)
*/
function throttle(func, wait, immediate = true) {
let timeout;
let previous = 0; // 记录上一次执行的时间戳
return function (...args) {
const context = this;
const now = Date.now();
if (immediate) {
// === 立即执行版 (时间戳逻辑) ===
// 如果当前时间 - 上次执行时间 > 等待时间,则执行
if (now - previous > wait) {
func.apply(context, args);
previous = now; // 更新执行时间
}
} else {
// === 延迟执行版 (定时器逻辑) ===
// 如果定时器不存在,说明当前时间段内还没有安排任务
if (!timeout) {
timeout = setTimeout(() => {
timeout = null; // 执行完清空,允许下一次调度
func.apply(context, args);
}, wait);
}
}
};
}
高级补充 :最完美的节流(如 Lodash)通常是
时间戳和定时器的结合体,能够做到"开始时立即执行" 且 "结束时执行最后一次"。但在手写面试中,清晰地写出上面两种逻辑的区分通常已经满分。
4. 总结
记住这两个词:
- 防抖 (Debounce) -> 延迟直到平静 (Delay until quiet)
- 节流 (Throttle) -> 固定频率 (Fixed frequency)