防抖就是频繁触发只执行最后一次,节流就是固定频率执行。
核心区别:
1. 防抖 debounce——一直输入不执行,停下来才执行(搜索框)
2. 节流 throttle——每隔 N 毫秒执行一次(滚动加载)
坑:防抖用 setTimeout + clearTimeout、节流用时间戳或定时器、resize/scroll/input 事件必加。
一句话区分:防抖 = 停了才执行最后一次(你停笔了才查);节流 = 固定间隔执行一次(每隔固定时间查一次)。选错了,要么接口请求爆炸,要么体验卡顿。
打个比方:你在电梯里按关门,每按一次它就重新开门等你——只要你还在按,它就不关。防抖就是这个思路:只要事件还在触发,就不执行,重新计时;你停下来了,它才执行最后一次。
// 防抖函数:把你的业务函数包一层,返回一个新的"带刹车"的函数
function debounce(fn, delay = 300) {
// 存定时器的编号
let timer = null;
// 返回一个新函数:以后调这个新函数,就有了防抖效果
return function () {
let args = arguments;
let self = this;
// ★ 每次触发先把上一次的定时器取消!
clearTimeout(timer);
// 就像电梯门刚要关,你又按了一下,重新等
// 重新开一个定时器,延迟 delay 毫秒后执行
timer = setTimeout(function () {
// 到时间了,真正执行业务函数
fn.apply(self, args);
timer = null;
}, delay);
};
}
// 用法:
function searchHandler(val) {
// 实际开发这里发 AJAX 请求
console.log("搜索了:" + val);
}
// 把 searchHandler 包成防抖版,300 毫秒内连续触发只执行最后一次
// 输入框绑定防抖版,快速打"hello"只发一次请求
const debouncedSearch = debounce(searchHandler, 300);
// 注意 id 要和 HTML 里的一致
document.getElementById("inpSearch").oninput = function () {
debouncedSearch(this.value);
};
如果你在输入框直接绑 searchHandler,打五个字母发五次请求——因为没有刹车,每按一个键就触发一次。用防抖包一层后,你停下来 300 毫秒才发一次,接口压力小多了。
节流的思路不同:不管你触发多频繁,我每隔固定时间只执行一次。就像闹钟,你不按它也会每隔固定时间响一次——不是"停了才响",而是"每隔一段时间响一次"。
// ===== 版本一:时间戳版(第一次立刻执行) =====
function throttle(fn, interval = 300) {
// 上一次执行的时间
let lastTime = 0;
return function () {
let args = arguments;
let self = this;
// 现在的时间戳
const now = Date.now();
// 如果距上次执行够间隔了,就执行
if (now - lastTime >= interval) {
fn.apply(self, args);
// 更新上次执行时间
lastTime = now;
}
};
}
// ===== 版本二:定时器版(第一次延迟执行) =====
function throttleTimer(fn, interval = 300) {
let timer = null;
return function () {
let args = arguments;
let self = this;
// 已经有定时器在排队了,就不动
if (timer) return;
timer = setTimeout(function () {
fn.apply(self, args);
// 执行完了,解锁
timer = null;
}, interval);
};
}
两个版本区别:时间戳版——第一次触发立刻执行,停止后不再执行最后一次;定时器版——第一次延迟执行,停止后还会执行最后一次。看场景选。
| 对比维度 | 防抖 debounce | 节流 throttle |
|---|---|---|
| 核心逻辑 | 频繁触发重置计时,停了才执行 | 固定间隔强制执行一次 |
| 执行时机 | 停止触发后延迟执行(延后) | 时间戳版:首次立即;定时器版:延后 |
| 触发频率 | 只执行最后一次 | 匀速分段执行 |
| 典型效果 | 输入停半秒才搜索 | 滚动时每隔一段距离加载 |
| 关键词 | clearTimeout 重置定时器 | 记录时间戳 / 锁住定时器 |
// 1. 输入框实时联想搜索 → 用防抖! // 你打字时不需要每打一个字母就搜一次,停下来再搜 // 减少接口请求次数,用户体验也好 // 2. 窗口缩放 resize → 用防抖! // 你拖窗口边框时会疯狂触发 resize,松手了再重新算布局 // 3. 页面滚动下拉加载 → 用节流! // 滚动是持续触发的,你需要每隔固定时间检测一次位置 // 不能等用户停了再加载——那样用户滚到中间啥都没有 // 4. 提交按钮防重复点击 → 用节流/短时防抖! // 用户快速点好几下提交,只发一次请求,防止提交多条数据
口诀:只需要结束后执行一次 → 防抖;需要过程中均匀执行 → 节流。
可能在什么地方用:
1. 搜索框输入联想(防抖)
2. 窗口拖动缩放重新布局(防抖)
3. 页面滚动到底自动加载更多(节流)
4. 按钮防重复提交(节流/防抖)。
常见的问题:
1. 防抖里忘了 clearTimeout,导致每次还是都执行
2. 节流用在搜索框上——刚打第一个字就立刻发了请求,用户还没打完就白跑一趟
3. 防抖用在滚动加载上——结果用户不滚了才加载,体验很差
4. 延迟时间设太长,用户等了一秒多才出结果。
解决思路:
1. 防抖核心就是 clearTimeout(timer) 重置计时
2. 搜索框用防抖(300ms 合适)
3. 滚动加载用节流(200~500ms 合适)
4. 按钮防重可以在提交期间把按钮 disabled。
练习一:防抖测试
1. 封装 debounce 函数
2. 做一个输入框,分别测试不加防抖和加防抖(300ms)时快速输入"hello"的打印次数差异。
练习二:节流测试
1. 封装 throttle 函数
2. 做一个 div,绑 mousemove 事件,分别测试不加节流和加节流(100ms)时鼠标移动的打印频率差异。
练习四:按钮防重复提交创建一个提交按钮,点击时 setTimeout 模拟 2 秒提交。快速点好几次,看发了几次请求。再用防抖/节流改造,对比哪种更合适。
| API | 作用 | 参数 | 返回值 | 代码示例 |
|---|---|---|---|---|
| setTimeout(fn,ms) | 过一会再执行一次 | 入参:函数、毫秒 | 返回:定时器 id | let id = setTimeout(function () { }, 1000); |
| clearTimeout(id) | 取消还没到点的定时 | 入参:定时器 id | 返回:无 | clearTimeout(id); |
| debounce(fn,ms) | 防抖:停手后才执行(输入联想) | 入参:函数、毫秒 | 返回:新函数 | let search = debounce(fn, 300); |
| throttle(fn,ms) | 节流:按节奏固定执行(滚动) | 入参:函数、毫秒 | 返回:新函数 | let scroll = throttle(fn, 200); |