在前端开发中,有些事件会被 "高频触发"------ 比如输入框打字时每秒触发多次keyup
,滚动页面时每秒触发几十次scroll
,快速点击按钮时瞬间触发多次click
。如果每次触发都执行复杂逻辑(如发送请求、计算布局),会严重拖慢页面,甚至导致卡顿。
防抖(debounce)和节流(throttle)是解决这类问题的两种经典方案。它们通过不同的策略控制函数执行频率,既能保证功能正常,又能大幅提升性能。
防抖(debounce):等 "安静" 下来再执行
(1)核心逻辑:短时间内多次触发,只执行最后一次
防抖的规则很简单:当函数被连续触发时,只有在停止触发后等待指定时间(delay),才会执行一次;如果在等待期间再次触发,就重新计时。 举个例子(delay=500ms):
- 用户在输入框快速打字,每次按键都会触发事件,但防抖会 "推迟" 执行,直到用户停手 500ms 后,才执行一次搜索请求;
- 如果用户在 300ms 内又按了下一个键,之前的计时会被取消,重新从 0 开始算 500ms。
生活类比:像是电梯关门 ------ 如果有人连续进入,电梯会不断推迟关门时间,直到最后一个人进入后,才会关门运行。
(2)实现代码与关键细节
javascript
function debounce(fn, delay) {
// 用闭包保存定时器ID,确保多次触发时能访问到同一个定时器
let timer = null;
// 返回一个新函数,接收触发时的参数
return function (...args) {
const that = this; // 保存当前上下文(如DOM元素)
// 如果已有定时器,先清除(重新计时)
if (timer) {
clearTimeout(timer);
}
// 重新设置定时器,delay毫秒后执行原函数
timer = setTimeout(() => {
// 用call确保原函数的this指向正确(如绑定到触发事件的DOM元素)
fn.call(that, ...args);
// 执行后清空定时器(非必需,但逻辑更清晰)
timer = null;
}, delay);
};
}
关键细节:
- 闭包的应用:通过
timer
变量在多次触发间共享状态,实现 "清除上一次定时器" 的逻辑; this
指向修正:用call(that, ...args)
确保原函数内部的this
指向正确(比如事件处理函数中,this
应指向触发事件的 DOM 元素);- 参数传递:用扩展运算符
...args
接收所有参数,保证原函数能拿到触发时的参数(如输入框的value
)。
(3)使用场景与实战示例
适用场景:
- 输入框实时搜索 / 联想:等待用户输入停顿后再发请求,减少接口调用次数;
- 窗口
resize
事件:窗口调整完成后再计算元素布局,避免多次重排; - 按钮防重复提交:用户快速点击按钮时,只在最后一次点击后执行提交逻辑。
实战代码(输入框) :
html
<input type="text" id="debounceInput" placeholder="防抖示例:输入后停顿500ms执行">
<script>
// 防抖函数(同上)
function debounce(fn, delay) { /* ... */ }
// 模拟搜索请求
function search(content) {
console.log(`[防抖] 搜索内容:${content}`);
}
// 生成防抖处理后的搜索函数(延迟500ms)
const debouncedSearch = debounce(search, 500);
// 绑定输入框事件
document.getElementById('debounceInput').addEventListener('keyup', function(e) {
debouncedSearch(e.target.value);
});
</script>
节流(throttle):固定间隔内必须执行一次
(1)核心逻辑:无论触发多频繁,固定间隔内只执行一次
节流的规则是:函数被触发后,立即执行一次;之后在指定时间(delay)内,无论触发多少次,都不会执行;直到 delay 时间过去,再次触发时才会执行第二次。
举个例子(delay=1000ms):
- 第一次触发时,立即执行函数,同时记录执行时间;
- 接下来 1 秒内,无论触发多少次,都不执行;
- 1 秒后再次触发,立即执行,并更新记录时间,以此类推。
与防抖的核心区别:
- 防抖:等待 "完全停止触发" 后才执行,可能长时间不执行;
- 节流:固定间隔内 "必须执行一次",保证函数有规律地执行。
实现代码与关键细节
javascript
function throttle(fn, delay) {
let lastTime = 0; // 记录上一次执行的时间(初始为0)
let timer = null; // 用于延迟执行的定时器
return function (...args) {
const that = this;
const now = Date.now(); // 当前时间戳
// 如果距离上一次执行不足delay,设置延迟执行
if (now - lastTime < delay) {
// 清除之前的定时器,避免重复延迟执行
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
lastTime = Date.now(); // 更新执行时间
fn.call(that, ...args);
timer = null;
}, delay - (now - lastTime)); // 计算剩余时间
} else {
// 距离上一次执行超过delay,立即执行
lastTime = now;
fn.call(that, ...args);
}
};
}
关键细节:
- 时间戳判断:通过
now - lastTime
计算与上次执行的间隔,决定是否立即执行; - 延迟执行兜底:当触发间隔小于 delay 时,用定时器保证 "在 delay 后必须执行一次"(避免因持续高频触发导致函数一直不执行);
- 闭包保存状态:
lastTime
和timer
在多次触发间共享,确保间隔计算准确。
(3)使用场景与实战示例
适用场景:
-
滚动事件
scroll
:计算滚动位置、加载懒加载图片时,每秒执行 1-2 次即可,无需高频触发; -
鼠标移动
mousemove
:拖拽元素时,固定间隔更新位置,避免过度计算; -
高频点击按钮:如游戏中的攻击按钮,限制每秒最多触发 5 次,防止操作过快。
实战代码 :
html
<input type="text" id="throttleInput" placeholder="节流示例:每1000ms最多执行一次">
<script>
// 节流函数(同上)
function throttle(fn, delay) { /* ... */ }
// 模拟搜索请求
function search(content) {
console.log(`[节流] 搜索内容:${content}`);
}
// 生成节流处理后的搜索函数(间隔1000ms)
const throttledSearch = throttle(search, 1000);
// 绑定输入框事件
document.getElementById('throttleInput').addEventListener('keyup', function(e) {
throttledSearch(e.target.value);
});
</script>
防抖与节流的核心区别与选择指南
特性 | 防抖(debounce) | 节流(throttle) |
---|---|---|
执行时机 | 停止触发后等待 delay 执行一次 | 触发后立即执行,之后固定间隔执行 |
适用场景 | 等待 "完成" 后执行(如输入完成) | 需要 "定期" 执行(如滚动计算) |
极端情况 | 若一直触发,可能永远不执行 | 无论是否一直触发,固定间隔必执行 |
选择原则:
- 若需要 "操作完成后执行一次"(如搜索输入),用防抖;
- 若需要 "操作过程中有规律地执行"(如滚动加载),用节流。
背后的核心知识点:闭包与高阶函数
防抖和节流的实现都依赖两个关键概念:
- 高阶函数 :
debounce
和throttle
都是高阶函数 ------ 它们接收一个函数(fn
)作为参数,并返回一个新函数。这使得它们能对原函数进行 "包装",添加额外的控制逻辑(如定时器)。 - 闭包 :返回的新函数通过闭包访问
timer
、lastTime
等变量,这些变量在多次触发间保持状态,实现 "清除定时器""计算时间间隔" 等核心逻辑。如果没有闭包,就需要将这些状态暴露为全局变量,导致代码污染和逻辑混乱。