防抖与节流:从输入框看性能优化
前言
在前端开发中,我们经常会遇到一些高频触发的事件——比如输入框的 keyup、窗口的 resize、页面的 scroll 等。如果每次事件都立即执行回调,往往会带来性能问题:频繁的 AJAX 请求加重服务器负担,复杂的 DOM 计算导致页面卡顿,甚至影响用户体验。
为了解决这类问题,前端工程师引入了两个经典的优化工具:防抖(debounce) 和 节流(throttle)。它们虽然名字相似,但原理和应用场景截然不同。很多初学者容易混淆这两个概念,或者只会用现成的库函数却不理解其内部机制。
本文将从一段最朴素的输入框代码开始,一步步带你发现性能问题的根源,然后通过手写防抖和节流函数(基于你提供的代码),并提供完整的可运行 HTML 示例,逐行注释解析它们的实现原理。最后通过对比表格和场景分析,帮你彻底搞懂这两个工具的区别与选择。你可以直接复制文中的 HTML 代码到本地运行,亲眼观察效果。
一、从一个简单输入框开始(无优化)
假设我们有一个搜索框,希望在用户输入时实时显示建议(suggest)。最直接的写法是监听 keyup 事件,每次按键都发起 AJAX 请求(这里用 console.log 模拟请求)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>无防抖/节流示例</title>
</head>
<body>
<h3>无防抖/节流:每次按键都会触发</h3>
<input type="text" id="undebounce" placeholder="输入内容" />
<script>
// 模拟 AJAX 请求
function ajax(content) {
console.log('ajax request', content);
}
const inputa = document.getElementById('undebounce');
inputa.addEventListener('keyup', function(e) {
ajax(e.target.value); // 每次按键都调用
});
</script>
</body>
</html>
将以上代码保存为 HTML 文件并在浏览器中打开,打开控制台(F12),然后在输入框中快速输入文字,你会看到每按一个键控制台就打印一条日志,瞬间输出大量内容。
效果图:
这样写会有什么问题?
当用户快速输入时(比如一秒内按下 10 个键),就会连续发起 10 个请求,而其中绝大部分请求是没必要的(因为用户还没输完)。这不仅浪费带宽,还会给服务器造成压力,甚至导致页面卡顿。
为了解决这类问题,我们需要一种机制,控制函数的执行频率——这就是防抖和节流的用武之地。
二、防抖(debounce):只执行最后一次
2.1 什么是防抖?
防抖的核心思想是:当你频繁触发事件时,只在最后一次触发后的指定时间后执行一次。如果在这段时间内再次触发,则重新计时。
还是拿搜索框举例:我们希望用户停止输入一段时间(比如 500ms)后再去请求建议,而不是每敲一个字都请求。
2.2 手写一个防抖函数(带详细注释)
下面是一个完整的 HTML 示例,它使用了用户提供的防抖函数,并加上了详细的注释。你可以直接运行并观察效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>防抖示例</title>
</head>
<body>
<h3>防抖:停止输入 500ms 后触发</h3>
<input type="text" id="debounce" placeholder="输入内容" />
<script>
// 模拟 AJAX 请求
function ajax(content) {
console.log('ajax request', content);
}
/**
* 防抖函数
* @param {Function} fn 需要执行的函数
* @param {number} delay 延迟时间(毫秒)
* @returns {Function} 返回一个具有防抖功能的新函数
*/
function debounce(fn, delay) {
// 利用闭包保存定时器ID,这样每次调用返回的函数时都能访问同一个变量
var id;
// 返回的函数就是实际绑定到事件上的处理函数
return function(args) {
// 如果已经有定时器,说明之前有过触发,取消它(重新计时)
if (id) {
clearTimeout(id);
}
// 保存当前的 this 上下文,因为在 setTimeout 回调中 this 会丢失
var that = this;
// 设置一个新的定时器,delay 毫秒后执行
id = setTimeout(function() {
// 在定时器回调中,通过 call 调用原函数,并传入正确的 this 和参数
fn.call(that, args);
}, delay);
};
}
const inputb = document.getElementById('debounce');
// 创建一个防抖版本的 ajax 函数,延迟 500ms
const debounceAjax = debounce(ajax, 500);
inputb.addEventListener('keyup', function(e) {
// 每次触发都调用防抖函数,参数是输入框当前值
debounceAjax(e.target.value);
});
</script>
</body>
</html>
运行说明:
在输入框中快速输入一串文字,然后停止。你会发现只有在停止输入 500ms 后,控制台才会打印一次请求内容。即使你输入过程中按键频率很高,也只会触发最后一次。
效果图:
快速输入后停止,控制台只有一条输出的截图
2.3 防抖逻辑可视化
假设用户快速输入了三次,每次间隔 200ms,delay=500ms:
- 第一次输入(0ms):
id为空,跳过if,设置定时器 A,计划在 500ms 后执行。 - 第二次输入(200ms):清除定时器 A,设置定时器 B,计划在 700ms 后执行(200+500)。
- 第三次输入(400ms):清除定时器 B,设置定时器 C,计划在 900ms 后执行(400+500)。
- 900ms 时:定时器 C 执行,调用
fn。
结果:只执行了一次,且是最后一次输入后的 500ms。
2.4 防抖的适用场景
- 搜索框建议
- 窗口 resize(等待调整完成后再计算)
- 表单验证(输入完成后验证)
- 按钮提交(防止重复提交,常结合立即执行选项)
三、节流(throttle):控制执行频率
3.1 什么是节流?
节流的核心思想是:无论事件触发的频率有多高,保证在单位时间内只执行一次。就像FPS游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
3.2 手写一个节流函数(用户提供的版本,带详细注释)
下面是一个完整的 HTML 示例,它使用了用户提供的节流函数,并加上了详细的注释。你可以直接运行并观察效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>节流示例(用户版本)</title>
</head>
<body>
<h3>节流:第一次立即执行,停止后执行最后一次</h3>
<input type="text" id="throttle" placeholder="输入内容" />
<script>
// 模拟 AJAX 请求
function ajax(content) {
console.log('发送请求,内容:', content);
}
/**
* 节流函数(用户提供的特殊版本:首次立即执行,连续触发只执行最后一次)
* @param {Function} fn 需要执行的函数
* @param {number} delay 间隔时间(毫秒)
* @returns {Function} 返回一个具有节流功能的新函数
*/
function throttle(fn, delay) {
// last: 上次执行函数的时间戳(毫秒)
// deferTimer: 定时器ID,用于延迟执行
let last, deferTimer;
// 返回的函数是实际的事件处理函数
return function() {
// 保存当前的 this 上下文,因为下面有 setTimeout
let that = this;
// 保存所有传入的参数(arguments 是类数组对象)
let _args = arguments;
// 获取当前时间戳,+new Date() 等效于 new Date().getTime()
let now = +new Date();
// 判断是否处于“冷却期”:
// last 存在(即已经执行过至少一次)并且 当前时间 < 上次执行时间 + 间隔
if (last && now < last + delay) {
// 还在冷却期内:清除之前设置的定时器(如果有)
clearTimeout(deferTimer);
// 设置一个新的定时器,在 delay 毫秒后执行
deferTimer = setTimeout(function() {
// 到达执行时刻:将 last 更新为触发时刻的时间戳(注意 now 是外层的值)
last = now;
// 使用 apply 调用原函数,传入保存的 this 和参数,停止输入后还要执行最后一次
fn.apply(that, _args);
}, delay);
} else {
// 首次调用 或 冷却期已过:立即执行
last = now; // 更新上次执行时间为当前时间戳
fn.apply(that, _args); // 立即执行原函数
}
};
}
const inputc = document.getElementById('throttle');
// 创建一个节流版本的 ajax 函数,间隔 500ms
const throttleAjax = throttle(ajax, 500);
inputc.addEventListener('keyup', function(e) {
throttleAjax(e.target.value);
});
</script>
</body>
</html>
运行说明:
- 在输入框中第一次按键,会立即看到控制台打印(第一次立即执行)。
- 接着快速连续输入,在输入过程中不会有任何打印。
- 停止输入后,等待 500ms,控制台会再打印一次(最后一次延迟执行)。
效果图:第一次立即执行一次,每隔相同时间执行一次,停止后还会执行最后一次
3.7 节流的适用场景
- 滚动加载(监听滚动位置)
- 鼠标移动、拖拽(如 Canvas 画笔)
- 播放进度条更新
- 按钮防连点(视需求可选防抖或节流)
四、防抖 vs 节流:一张表看懂区别
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行策略 | 只执行最后一次 | 每隔一段时间执行一次 |
| 典型实现 | 每次触发重置定时器 | 使用时间戳或定时器锁 |
| 代码示例 | debounce(ajax, 500) | throttle(ajax, 500) |
| 行为描述 | 疯狂输入后停 500ms 执行一次 | 第一次立即执行,之后每 500ms 至少一次 |
| 核心逻辑 | 清除之前的定时器,重新设置 | 判断时间间隔或锁状态 |
| 适合场景 | 输入搜索、窗口 resize、表单验证 | 滚动加载、鼠标移动、动画控制 |
| 类比 | 电梯关门 | 技能冷却 |
五、如何选择?
- 当你关心最终状态时,用防抖(比如用户最终输入了什么)。
- 当你需要持续反馈但又要控制频率时,用节流(比如滚动位置、鼠标移动)。
在实际项目中,Lodash 提供了成熟的 _.debounce 和 _.throttle 函数,支持更多选项(如立即执行、取消等)。但理解手写实现能帮你更透彻地掌握闭包、定时器和 this 的用法。
六、总结
- 防抖:只执行最后一次,适合输入、调整等场景。
- 节流:均匀执行,适合滚动、动画等场景。
- 两者都是通过闭包保存状态(定时器ID/上次执行时间)来控制执行频率。
- 用户提供的防抖代码清晰展示了闭包和定时器的配合;节流代码则演示了一种特殊的行为(首次立即 + 最后一次延迟),理解它有助于区分不同实现间的细微差别。
希望本文能帮你理清防抖和节流的原理与区别,现在就去优化你的项目吧!
如果你觉得本文有帮助,欢迎点赞收藏,有任何问题可以在评论区交流~