玩游戏看60秒广告只需要完整看一次就行了,退出去一次重新60秒,别一直退
✅ 核心思想: 在事件被触发后,等待一段时间,如果这段时间内没有再次触发,才执行。
闪现CD:不管多急,都要等冷却好才能再次使用
✅ 核心思想: 多次触发 → 只按固定频率执行, 在一定时间内只允许执行一次操作,限制频率。
场景 | 使用 |
---|---|
搜索框输入 | ✅ 防抖(防止频繁请求) |
窗口 resize | ✅ 防抖或节流(根据需求) |
页面滚动监听 | ✅ 节流(避免频繁触发) |
表单重复提交 | ✅ 节流(防止多次点击) |
用户疯狂点击退出按钮
↓
启动定时器,广告倒计时500ms(广告要5秒钟看完)
↓
如果在倒计时内又点了一次(中途退出)
↓
清除上一个定时器,重新开始倒计时(重新看5秒广告)
↓
直到最后500ms内没人点了(不敢退了)
↓
终于执行真正的函数(看完广告,开始免费听歌)
第一次点击 → 执行函数,记录当前时间
↓
接下来的点击 → 判断距离上次执行是否超过指定间隔
↓
没超过 → 不执行
↓
超过了 → 再执行一次,并更新时间
特性 | 防抖 debounce | 节流 throttle |
---|---|---|
是否立即执行 | ❌ 默认不会,可加 immediate 参数控制 |
✅ 可以设置首次立即执行 |
定时器数量 | 1 个 | 1 个 |
是否有返回值 | 否(除非封装 Promise) | 否 |
是否需要清除定时器 | ✅ 是 | ✅ 是 |
常见应用场景 | 输入搜索、窗口调整 | 滚动加载、拖拽、高频点击 |
// 防抖函数,用于限制函数的执行频率
function debounce(fn, delay = 300, immediate = false) {
// 定时器变量,用于存储setTimeout的返回值
let timer = null;
// 标记函数是否已立即执行过(仅在immediate为true时有效)
let called = false;
// 返回一个新的函数,该函数会包裹原始函数fn
return function (...args) {
// 保存当前函数的this上下文,确保在回调中能正确使用
const context = this;
// 如果存在未执行的定时器,则清除它(取消上一次的延迟执行)
if (timer) clearTimeout(timer);
// 如果配置了立即执行且当前尚未执行过
if (immediate && !called) {
// 立即执行原始函数,并绑定正确的this和参数
fn.apply(context, args);
// 标记为已执行
called = true;
return; // 直接返回,避免后续的定时器设置
}
// 设置新的定时器,延迟执行原始函数
timer = setTimeout(() => {
// 延迟执行原始函数,绑定正确的this和参数
fn.apply(context, args);
// 重置执行标记(允许下次再次立即执行)
called = false;
}, delay);
};
}
delay
毫秒后才会执行函数。immediate: true
):
delay
内的调用会被防抖called
标记,允许下次再次立即执行...args
收集所有传入参数,并通过apply
确保参数正确传递。const context = this
保存调用时的this值,确保回调中的this与原始调用上下文一致。<input type="text" id="search" placeholder="输入内容搜索">
const input = document.getElementById('search');
input.addEventListener('input', debounce((e) => {
console.log('发送请求:', e.target.value);
}, 500));
// 节流函数,用于限制函数的执行频率,确保在指定时间间隔内最多执行一次
function throttle(fn, delay = 300, leading = true, trailing = true) {
// 记录上次执行的时间戳
let lastTime = 0;
// 定时器变量,用于存储setTimeout的返回值
let timer = null;
// 保存函数执行时的this上下文
let context;
// 保存函数执行时的参数
let args;
// 延迟执行的回调函数
const later = () => {
// 如果允许尾部执行(trailing为true)
if (trailing) {
// 更新最后执行时间
lastTime = Date.now();
// 清除定时器引用
timer = null;
// 执行原始函数
fn.apply(context, args);
}
};
// 返回包裹函数
return function wrapper(...params) {
// 保存当前函数的this上下文
context = this;
// 保存当前函数的参数
args = params;
// 获取当前时间戳
const now = Date.now();
// 如果从未执行过且不允许头部执行(leading为false)
if (!lastTime && !leading) {
// 设置初始执行时间为当前时间
lastTime = now;
}
// 计算距离上次执行剩余的时间
const remaining = delay - (now - lastTime);
// 如果剩余时间<=0(可以立即执行)或者首次执行
if (remaining <= 0 || remaining > delay) {
// 如果存在未执行的定时器,则清除它
if (timer) {
clearTimeout(timer);
timer = null;
}
// 更新最后执行时间
lastTime = now;
// 执行原始函数
fn.apply(context, args);
}
// 如果允许尾部执行且没有定时器正在等待
else if (!timer && trailing) {
// 设置定时器,在剩余时间后执行later函数
timer = setTimeout(later, remaining);
}
};
}
leading: true
):立即执行第一次调用trailing: true
):在停止调用后的延迟时间结束时执行context
和args
,确保在回调中能正确使用调用时的上下文和参数。timer
变量跟踪未执行的定时器,确保不会重复设置定时器。leading
为false的情况,以及剩余时间大于delay的特殊情况。使用示例:
<div style="height: 2000px;">滚动页面看控制台div>
window.addEventListener('scroll', throttle(() => {
console.log('页面正在滚动...');
}, 1000));
setInterval
?A:因为
setInterval
是周期性执行,而防抖的关键在于“每次触发都要重新计时”。
A:添加
immediate
参数,在第一次调用时立即执行,并在之后的触发中忽略,直到冷却期结束。
A:当然可以,通过
leading
和trailing
控制即可。
A:当然可以,比如:
v-debounce
/ v-throttle
useDebounce
/ useThrottle
Hook“我以前写了个输入框搜索功能,没加防抖,结果服务器直接宕机了,现在想想还觉得对不起运维。”
“节流就像我的工资,一个月只能发一次,不管你干了多少活。”
“防抖和节流就像是 JavaScript 中的时间管理大师,它们让你的代码不再‘焦虑’,也不再‘暴躁’。”
✅ 防抖(debounce):冷静一下,别急着执行。
✅ 节流(throttle):给我规矩点,不能太频繁。
技术方向 | 推荐做法 |
---|---|
Vue 方向 | 写成自定义指令,如 v-debounce="search" |
React 方向 | 写成 Hook,如 useDebounce(searchTerm, 500) |
TypeScript | 加类型注解,提升健壮性 |
性能优化 | 结合 requestIdleCallback、requestAnimationFrame |