本文深入解析JavaScript中节流(throttle)与防抖(debounce)函数的核心原理与应用场景。通过对比resize、scroll等高频事件,阐述两者在控制执行频率与空闲等待上的区别,并提供从基础实现到underscore.js源码级的代码示例,帮助开发者优化前端性能,避免频繁DOM操作导致的卡顿。
高频事件触发的性能问题
问题的引出
在Web开发中,某些交互场景下事件会被高频触发,导致DOM操作或资源加载过于频繁,进而引发页面UI卡顿甚至浏览器无响应。
针对此类问题,常见的需求通常分为两类:一是停止触发操作后等待指定毫秒数再执行后续逻辑;二是以固定的频率间隔执行处理函数。为了解决这两种不同的业务场景,分别衍生出了debounce(防抖)和throttle(节流)两种技术方案。
1. resize事件
2. mousemove事件
3. touchmove事件
4.scroll事件
节流与防抖的核心概念
throttle 与 debounce
目前主流JavaScript框架及工具库均提供了这两个函数的支持。例如,jQuery中有对应的throttle和debounce插件,而underscore.js和Lodash.js等库也原生内置了这两个函数。
原理:
其核心思路是设置一个时间阈值delay,确保在delay毫秒内,函数最多只执行一次。
关键在于执行时机的选择:是在触发事件后立即执行,然后进入等待期?还是先等待delay毫秒,若期间无新事件则执行函数?
在等待期间触发的事件被忽略,确保只执行一次处理逻辑。
具体策略上,存在两种模式:一种是在触发时立即执行一次,后续事件被忽略(执行在前);另一种是忽略前面的所有触发,仅在操作结束后执行最后一次(执行在后)。
throttle与debounce的区别
区别:
1. throttle
可以将节流形象地理解为拧紧水龙头,使水以水滴形式流出。你会发现每隔一段时间,就会有一滴水落下。
即预先设定一个执行周期,当调用动作的时刻大于等于执行周期时,则执行该动作,随后进入下一个新的周期。
2.debounce
可以将防抖形象地理解为按住弹簧,只要手不松开,弹簧就不会弹起;只有当手松开(停止触发)后,弹簧才会反弹。
即当调用动作n毫秒后,才会执行该动作;若在这n毫秒内再次调用此动作,则重新计算执行时间(重置计时器)。
代码实现与效果演示
简单代码实现及实验结果
下面通过简单的代码示例来演示这两个函数的具体行为:
throttle 函数:
window.addEventListener("resize", throttle(callback, 300, {leading:false}));
window.addEventListener("resize", callback2);
function callback () { console.count("Throttled"); }
function callback2 () { console.count("Not Throttled"); }
/**
* 频率控制函数, fn执行次数不超过 1 次/delay
* @param fn{Function} 传入的函数
* @param delay{Number} 时间间隔
* @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
* 如果想忽略结束边界上的调用则传入 {trailing:false},
* @returns {Function} 返回调用函数
*/
function throttle(fn,delay,options) {
var wait=false;
if (!options) options = {};
return function(){
var that = this,args=arguments;
if(!wait){
if (!(options.leading === false)){
fn.apply(that,args);
}
wait=true;
setTimeout(function () {
if (!(options.trailing === false)){
fn.apply(that,args);
}
wait=false;
},delay);
}
}
}
将上述代码粘贴到浏览器控制台运行,观察控制台输出结果,可得到:

接下来观察debounce函数的运行效果。
debounce 函数:
window.addEventListener("resize", throttle(callback, 300, {leading:false}));
window.addEventListener("resize", callback2);
function callback () { console.count("Throttled"); }
function callback2 () { console.count("Not Throttled"); }
/**
* 空闲控制函数, fn仅执行一次
* @param fn{Function} 传入的函数
* @param delay{Number} 时间间隔
* @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
* 如果想忽略结束边界上的调用则传入 {trailing:false},
* @returns {Function} 返回调用函数
*/
function debounce(fn, delay, options) {
var timeoutId;
if (!options) options = {};
var leadingExc = false;
return function() {
var that = this,
args = arguments;
if (!leadingExc&&!(options.leading === false)) {
fn.apply(that, args);
}
leadingExc=true;
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(function() {
if (!(options.trailing === false)) {
fn.apply(that, args);
}
leadingExc=false;
}, delay);
}
}
将上述代码粘贴到浏览器中运行,并分三次改变窗口大小。观察发现,每一次改变窗口大小的开始和结束边界,事件处理函数各执行一次:

如果是一次性连续改变窗口大小,会发现开始和结束的边界各执行一次事件处理函数。请注意对比一次性改变窗口大小时throttle的情况:

主流库源码解析
underscore.js 的代码实现
_.throttle函数
/**
* 频率控制函数, fn执行次数不超过 1 次/delay
* @param fn{Function} 传入的函数
* @param delay{Number} 时间间隔
* @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
* 如果想忽略结束边界上的调用则传入 {trailing:false},
* @returns {Function} 返回调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
_.debounce函数
/**
* 空闲控制函数, fn仅执行一次
* @param fn{Function} 传入的函数
* @param delay{Number} 时间间隔
* @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
* 如果想忽略结束边界上的调用则传入 {trailing:false},
* @returns {Function} 返回调用函数
*/
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
参考的文章
Debounce and Throttle: a visual explanation jQuery throttle / debounce: Sometimes, less is more! underscore.js
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。