前几天,我在写一个搜索功能时碰到了一个小问题:用户每输入一个字母,页面就立刻发一次请求。
只是轻轻的打个手机两个字,后台收到了四次查询,这样子服务器的压力可不小啊。
于是就想到了JS的防抖(Debounce) 和 节流(Throttle)。
这是两种常用的性能优化技术,主要用于控制函数的执行频率,比如 resize、scroll、input 等情况。
概念区别
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 核心思想 | 在连续触发事件时,只在最后一次触发后等待指定时间再执行 | 在连续触发事件时,保证在指定时间间隔内只执行一次 |
| 适用场景 | 搜索框输入、窗口 resize 后只执行一次计算 | 滚动加载、按钮点击限制、鼠标移动追踪等 |
| 是否立即执行 | 可选(可实现"立即执行"或"延迟执行") | 通常首次立即执行,之后按固定间隔 |
防抖(Debounce)实现
举个例子:
你在淘宝搜索"蓝牙耳机"的时候:
- 打"蓝" → 不急着搜
- 打"蓝牙" → 还是不搜
- 打完"蓝牙耳机"并停顿 0.5 秒 → 此时才发起搜索请求
这样既省流量,又避免服务器压力过大。
基础版本(延迟执行)
js
function debounce(func, delay) {
let timer = null; // 用来记录倒计时的变量
return function (...args) {
// 每次触发,先取消之前的倒计时
clearTimeout(timer);
// 重新开始一个新的倒计时
timer = setTimeout(() => {
func.apply(this, args); // 时间到了,执行真正的函数
}, delay);
};
}
使用示例:搜索框优化
html
<input id="search" placeholder="请输入关键词" />
js
const input = document.getElementById('search');
// 包装一个"防抖版"的搜索函数
const debouncedSearch = debounce(function(e) {
console.log('正在搜索:', e.target.value);
// 这里可以调用 API 发请求
}, 500); // 500毫秒 = 0.5秒
input.addEventListener('input', debouncedSearch);
实现效果:用户快速输入"hello"时,只在输入完"o"并停顿0.5秒后才发送请求。
而不是输入h、e、l、l、o时发送5次请求!
进阶版本(支持立即执行)
js
function debounce(func, delay, immediate = false) {
let timer = null;
return function (...args) {
// 判断是否应该"立即执行"
const callNow = immediate && !timer;
// 清除之前的定时器(防抖核心)
clearTimeout(timer);
// 设置新的延迟定时器
timer = setTimeout(() => {
timer = null; // 定时器执行完后重置
if (!immediate) func.apply(this, args); // 如果不是立即模式,就在延迟后执行
}, delay);
// 如果是立即模式且当前没有 pending 的定时器,则立刻执行
if (callNow) func.apply(this, args);
};
}
参数说明:
immediate: true:第一次触发时立即执行,之后在停止触发delay时间后不再执行。immediate: false(默认):只有在停止触发delay时间后才执行。
使用场景:
immediate: true:按钮提交,第一次点击立即执行,防止连续点击。immediate: false:搜索框,等用户输入完成再搜索。
节流(Throttle)实现
举个例子:
你在玩一个"自动存档"的游戏:
- 游戏每 5 秒自动保存一次进度;
- 即使你狂按"存档键",系统也只认每 5 秒那次。
基础代码实现(时间戳版)
js
function throttle(func, delay) {
let lastTime = 0; // 上次执行的时间
return function (...args) {
const now = Date.now(); // 当前时间
// 如果距离上次执行已经超过 delay 毫秒
if (now - lastTime >= delay) {
func.apply(this, args); // 执行函数
lastTime = now; // 更新"上次执行时间"
}
};
}
使用示例:监听页面滚动
js
const throttledScroll = throttle(function() {
console.log('页面滚动了!当前滚动位置:', window.scrollY);
}, 300); // 每300毫秒最多执行一次
window.addEventListener('scroll', throttledScroll);
无论你怎么疯狂滚动,函数每 0.3 秒最多执行一次,大大减少计算量。
定时器方式(延迟执行)
js
function throttle(func, delay) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
第一次触发也要等待delay时间后才执行
更完善的节流(首尾都执行)
js
function throttle(func, delay) {
let timer = null;
let lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
// 距离上次执行时间已超过delay,立即执行
if (timer) {
clearTimeout(timer);
timer = null;
}
func.apply(this, args);
lastTime = now;
} else if (!timer) {
// 设置定时器,在剩余时间后执行
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
lastTime = Date.now();
}, remaining);
}
};
}
这个版本的优点:
- 第一次触发立即执行(用户体验好)
- 最后一次触发也确保执行(不丢失重要操作)
- 中间按固定频率执行(性能优化)
适用场景: 需要同时保证响应性和性能的场景,如实时数据更新、动画效果等。
使用示例
1. 滚动加载优化
javascript
const checkScroll = throttle(() => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
// 距离底部100px时加载更多
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreContent();
}
}, 250);
window.addEventListener('scroll', checkScroll);
2. 窗口调整的响应式处理
javascript
const handleResize = debounce(() => {
// 重新计算布局
calculateLayout();
// 更新图表尺寸
updateCharts();
}, 300);
window.addEventListener('resize', handleResize);
总结
如何选择?
| 场景 | 推荐技术 | 原因 |
|---|---|---|
| 搜索框输入 | 防抖 | 只关心用户最终输入的内容 |
| 窗口调整 | 防抖 | 只关心调整结束后的最终尺寸 |
| 页面滚动 | 节流 | 需要实时响应但又要控制频率 |
| 按钮点击 | 节流 | 防止重复提交,但要给用户及时反馈 |
| 鼠标移动 | 节流 | 需要实时跟踪但避免过于频繁 |
防抖 :等你不再操作了我才执行。 节流:再怎么频繁,我也按节奏来。
两者都能减少函数调用次数,提升性能,但适用场景不同,可以根据需求选择。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》