在前端开发中,我们经常需要处理一些高频触发的事件,比如:
- 输入框搜索建议(
input
或keyup
) - 窗口调整大小(
resize
) - 滚动事件(
scroll
) - 鼠标移动(
mousemove
)
这些事件如果每次都执行某些代价较高的操作(如发起网络请求、重排重绘页面等),会对性能造成严重影响。为了解决这个问题,我们可以使用 防抖(debounce) 和 节流(throttle) 技术。
🧠 一、什么是防抖(Debounce)?
定义:
防抖 是指,在一段时间内多次触发同一个函数,只有最后一次触发后经过指定时间没有再次触发,才会真正执行该函数。
类比:
就像你在打字时,搜索引擎不会每次按键都去请求服务器,而是等你停下来后再请求。 就像你打游戏的回城,自己移动了或被打断了就只能重新回城,直到停下来等了指定时间才会真正回城成功。
应用场景:
- 搜索框输入实时建议
- 表单验证
- 窗口调整尺寸
- 多次点击按钮(避免重复提交)
✅ 示例代码:
javascript
function debounce(fn, delay) {
return function (args) {
const that = this;
clearTimeout(fn.id);
fn.id = setTimeout(() => {
fn.call(that, args); // 保留 this 上下文
}, delay);
};
}
📌 使用示例:
javascript
const inputA = document.getElementById('inputA');
inputA.addEventListener('keyup', debounce(function(e) {
console.log('发送请求:', e.target.value);
}, 300));
🧠 二、什么是节流(Throttle)?
定义:
节流 是指,在一定时间间隔内只允许执行一次函数。无论在这段时间内触发多少次,函数只会执行一次。
类比:
就像游戏中的技能冷却,每 5 秒只能释放一次技能。
应用场景:
- 页面滚动加载更多内容(如瀑布流)
- 窗口调整大小(避免频繁布局计算)
- 实时位置更新(如地图定位)
✅ 示例代码:
javascript
function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = +new Date();
if (!last || now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
}
📌 使用示例:
javascript
window.addEventListener('resize', throttle(function() {
console.log('窗口大小变化');
}, 200));
🔁 三、防抖 vs 节流:对比总结
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
原理 | 在规定时间内未被再次触发才执行 | 固定时间只执行一次 |
触发频率 | 多次触发 → 只执行最后一次 | 多次触发 → 每隔一段时间执行一次 |
典型用途 | 搜索建议、表单校验 | 滚动监听、窗口调整、动画帧控制 |
💡 四、进阶技巧与常见问题
1. 如何保证 this
不丢失?
在对象方法或事件回调中,this
很容易指向全局对象(如 window
)。解决方案有:
✅ 方法一:用变量保存 this
javascript
obj.inc = debounce(function(val) {
const that = this;
setTimeout(function () {
that.count += val;
}, 500);
});
✅ 方法二:使用 .bind()
fn.bind(that)(args);
✅ 方法三:使用箭头函数(推荐)
javascript
setTimeout(() => {
fn(args);
}, delay);
箭头函数不绑定自己的 this
,继承外层函数的 this
,非常适用于事件处理和定时器。
2. 如何兼容箭头函数和普通函数?
如果你传入的是一个箭头函数作为 fn
,也要注意它的 this
绑定行为。通常我们会优先使用箭头函数来简化上下文管理。
3. 如何封装成可复用的工具函数?
你可以将防抖和节流函数封装到一个通用工具库中,例如:
javascript
// utils.js
export function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
export function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = +new Date();
if (now - last > delay) {
fn.apply(this, args);
last = now;
}
};
}
然后在组件中导入使用:
javascript
import { debounce } from './utils';
input.addEventListener('input', debounce(fetchSuggestions, 300));
📈 五、实际应用案例分析
1. 输入框搜索建议(防抖)
html
<input type="text" id="searchInput">
<script>
const input = document.getElementById('searchInput');
function fetchSuggestions(query) {
console.log('发送请求:', query);
}
input.addEventListener('input', debounce((e) => {
fetchSuggestions(e.target.value);
}, 300));
</script>
2. 图片懒加载 + 滚动监听(节流)
html
<img data-src="image1.jpg" class="lazy-img">
<script>
function lazyLoadImages() {
const images = document.querySelectorAll('.lazy-img');
images.forEach(img => {
if (isInViewport(img)) {
img.src = img.dataset.src;
}
});
}
window.addEventListener('scroll', throttle(lazyLoadImages, 200));
</script>
关于图片懒加载,想详细了解可看前端性能优化实战:一文搞懂图片懒加载(Lazy Load),从原理到代码全解析在图片泛滥的网页时代,加载速度决定用户体验 - 掘金
🧩 六、拓展知识:高阶函数 & 闭包的应用
1. 高阶函数(Higher-order Function)
防抖和节流函数都是典型的高阶函数,它们:
- 接收一个函数作为参数
- 返回一个新的函数(包装后的函数)
这种设计模式广泛应用于现代 JS 开发中,尤其在 React、Vue 等框架中。
2. 闭包(Closure)
在防抖和节流函数中,我们利用了闭包来保存状态(如 timer
、last
、fn.id
等),这使得函数可以在多次调用之间共享状态,而不污染全局作用域。
🎯 七、结语:何时用防抖?何时用节流?
场景 | 推荐技术 |
---|---|
用户输入搜索建议 | ✅ 防抖 |
窗口调整大小 | ✅ 节流 |
滚动加载更多内容 | ✅ 节流 |
频繁点击按钮 | ✅ 防抖 |
实时数据同步(如聊天输入) | ✅ 防抖 |
动画帧控制 | ✅ 节流 |