前言
最近有一次面试,被狠狠拷打了,尤其是在讨论到页面性能优化时,关于防抖与节流的问题让我措手不及。为了避免再次遇到同样的尴尬,我决定系统地总结这方面的知识,力求简洁明了地分享出来。
一、认识防抖和节流
防抖(Debouncing)
防抖技术的核心思想是在一个指定的时间间隔内,如果事件被频繁触发,则只有最后一次触发才会执行相应的回调函数。换句话说,它确保了在连续快速的操作中,只执行一次回调。这对于避免不必要的计算或请求特别有用,比如用户在搜索框输入时,我们不希望每次按键都发送请求,而是等待用户停止输入一段时间后再发送。
节流(Throttling)
不同于防抖,节流限制了一个函数在一定时间内的调用次数。即无论事件被触发多少次,在规定的时间间隔内只会执行一次回调。例如,在处理滚动事件时,我们可以设置每秒只响应一次,从而减少浏览器的负担。
这两种技术的区别
- 防抖:就像游戏里面的回城操作,如果在回城时间内再次点击回城(回城被打断),则会重新计算回城时间。也就是说,防抖会在你停止操作后的一段时间才执行。
- 节流:类似于游戏里英雄的普通攻击,在一定时间内点击多次,也只能攻击一次。这意味着在设定的时间间隔内,即使事件被多次触发,也只会执行一次回调。
使用场景分析
-
防抖:
- 搜索框自动完成建议:用户输入完毕后延迟一段时间再发送请求,避免每次按键都发送请求。
- 表单验证:在用户停止输入后进行表单验证,而不是每次按键都验证。
- 窗口大小调整:在用户停止调整窗口大小后执行相应的布局调整代码。
-
节流:
- 页面滚动加载更多内容:当用户滚动页面时,每隔一定时间检查是否需要加载更多数据。
- 鼠标移动事件:在游戏中跟踪玩家的鼠标移动,但不需要对每一次微小的移动都做出反应。
- 触摸屏滑动:在触摸屏设备上,监听用户的滑动操作,但不需要对每一个细微的触摸点变化都做出响应。
二、实现防抖
防抖的实现原理非常简单,就是通过对要执行的函数进行延迟处理,以此来控制函数执行的次数。
具体流程如下:
- 定义
debounce函数:接受两个参数,一个是需要执行的函数func,另一个是等待的时间wait。 - 内部逻辑:
- 使用
let timeout;来存储定时器ID。 - 返回一个新的函数,每当触发事件时调用此函数。
- 在每次调用返回的函数时,首先清除之前设置的定时器(如果有)。
- 设置一个新的定时器,在指定的
wait时间后执行传入的func函数,并使用apply方法确保正确的上下文(this指向)和参数传递。
- 使用
// 防抖
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
三、实现节流
节流技术的实现原理也非常简单,就是通过设置一个固定时间间隔,在这个时间间隔内只能执行一次相应的回调函数。
简单实现流程如下:
- 定义
throttle函数:接受两个参数,一个是需要执行的函数func,另一个是限制的时间间隔limit。 - 内部逻辑:
- 使用
let lastExecutionTime = 0;来记录上一次执行的时间戳。 - 返回一个新的函数,每当触发事件时调用此函数。
- 在每次调用返回的函数时,获取当前时间
now。 - 如果当前时间减去上次执行的时间大于或等于设定的
limit,则执行传入的func函数,并更新lastExecutionTime为当前时间。
- 使用
// 节流
function throttle(func, limit) {
let lastExecutionTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastExecutionTime >= limit) {
func.apply(this, args);
lastExecutionTime = now;
}
};
}
高级实现:
function throttle(func, wait, options = {}) {
let timeout = null;
let previous = 0;
return function (...args) {
const context = this;
const now = Date.now();
// 如果设置了不立即执行,并且是第一次触发,则跳过
if (!previous && options.leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
// 如果剩余时间小于等于 0,或者剩余时间大于等待时间(系统时间被调整)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout && options.trailing !== false) {
// 如果没有计时器,并且设置了尾部执行
timeout = setTimeout(() => {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
func.apply(context, args);
}, remaining);
}
};
}
参数说明:
func:需要节流的函数。wait:时间间隔(毫秒)。options:配置对象,包含以下属性:leading:是否在节流开始时立即执行(默认true)。trailing:是否在节流结束后执行(默认true)。
示例:
window.addEventListener('scroll', throttle(() => {
console.log('Scrolling...');
}, 200));
四. 高级用法
(1)结合 requestAnimationFrame
对于动画相关的场景,可以使用 requestAnimationFrame 实现更平滑的节流。
function throttleWithRAF(func) {
let isThrottled = false;
return function (...args) {
const context = this;
if (!isThrottled) {
requestAnimationFrame(() => {
func.apply(context, args);
isThrottled = false;
});
isThrottled = true;
}
};
}
(2)动态调整等待时间
可以根据事件的触发频率动态调整防抖或节流的等待时间。
function dynamicDebounce(func, minWait, maxWait) {
let timeout = null;
let lastCallTime = 0;
return function (...args) {
const context = this;
const now = Date.now();
const elapsed = now - lastCallTime;
if (elapsed < minWait) {
clearTimeout(timeout);
timeout = setTimeout(() => {
lastCallTime = now;
func.apply(context, args);
}, maxWait);
} else {
lastCallTime = now;
func.apply(context, args);
}
};
}
(3)取消功能
为防抖和节流函数添加取消功能。
function debounce(func, wait) {
let timeout = null;
function debounced(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
结语
掌握这些技巧不仅能够让你的网页运行得更加流畅,还能在面试中展现出你的专业水平和技术深度。希望本文能为你的前端开发之路提供一些有价值的参考!