JavaScript性能优化:函数节流(throttle)与函数去抖(debounce)详解

作者:安全边界

时间:2026-10-30

浏览:450

本文深入解析JavaScript中函数节流(throttle)与函数去抖(debounce)的核心原理及实现。通过对比resize、scroll等高频事件场景,阐述两种策略在控制执行频率与延迟执行上的差异。文章提供简单封装代码,并剖析Underscore库中相关源码,帮助开发者提升前端性能,避免浏览器卡顿。

文章目录

函数节流与去抖的基本概念

函数节流的核心机制在于限制函数的连续调用频率。具体而言,它确保在设定的时间间隔内,函数只能执行一次。只有当上一次执行完毕后,经过规定的时间间隔,下一次调用才会被允许触发。

实现函数节流的基本思路是利用定时器。当触发事件时,首先设置一个定时器延迟执行逻辑。若在延迟期间再次触发事件,则清除原有的定时器,并重新设置一个新的定时器。通过这种方式,确保函数在指定的时间窗口内只执行一次。

在Web开发中,某些场景下事件会被频繁触发,导致DOM操作、资源加载等高开销行为重复执行。这不仅会造成用户界面卡顿,严重时甚至可能导致浏览器崩溃。

1. window对象的resize、scroll事件

2. 拖拽时的mousemove事件

3. 射击游戏中的mousedown、keydown事件

4. 文字输入、自动完成的keyup事件

高频事件场景与策略选择

针对window的resize事件,常见需求是在停止改变大小n毫秒后执行后续处理;而其他如mousemove等事件,通常需求是以固定频率执行。针对这两种不同的业务场景,分别衍生出了debounce(去抖)和throttle(节流)两种解决方案。

throttle和debounce是解决前端请求与响应速度不匹配问题的两种主要策略。它们的核心区别在于对执行时机的选择策略不同。

throttle(节流)的核心逻辑是:无论事件触发多么频繁,函数都按照固定的时间间隔执行。

debounce(去抖)的核心逻辑是:在时间间隔t内,如果再次触发事件,则重新计时。只有当停止触发事件的时间大于或等于t时,函数才会执行。

基础实现与调用示例

一、throttle函数的简单实现


function throttle(fn, threshhold, scope) { 
threshhold || (threshhold = 250); 
var last, 
timer; return function () { 
var context = scope || this; 
var now = +new Date(), 
args = arguments; 
if (last && now - last + threshhold < 0) { 
// hold on to it 
clearTimeout(deferTimer); 
timer = setTimeout(function () { 
last = now; 
fn.apply(context, args); 
}, threshhold); 
} else { 
last = now; 
fn.apply(context, args); 
} 
};}

调用方法


$('body').on('mousemove', throttle(function (event) 
{
console.log('tick');
}, 1000));

二、debounce函数的简单实现


function debounce(fn, delay) 
{ 
var timer = null; 
return function () 
{ 
var context = this,
args = arguments; 
clearTimeout(timer); 
timer = setTimeout(function () { 
fn.apply(context, args); 
}, delay); 
};}

调用方法


$('input.username').keypress(debounce(function (event)
{
// do the Ajax request
}, 250));

三、简单的封装实现


/** * throttle * @param fn, wait, debounce */var throttle = function ( fn, wait, debounce ) { 
var timer = null, // 定时器 
t_last = null, // 上次设置的时间 
context, // 上下文 
args, // 参数 
diff; // 时间差 
return funciton () { 
var curr = + new Date(); 
var context = this, args = arguments; 
clearTimeout( timer ); 
if ( debounce ) { // 如果是debounce 
timer = setTimeout( function () { 
fn.apply( context, args ); 
}, wait ); 
} else { // 如果是throttle 
if ( !t_last ) t_last = curr; 
if ( curr - t_last &gt;= wait ) { 
fn.apply( context, wait ); 
context = wait = null; 
} 
} }}/** * debounce * @param fn, wait */var debounce = function ( fn, wait ) 
{ 
return throttle( fn, wait, true );
}

小结:这两个方法适用于会重复触发的一些事件,如:mousemove,keydown,keyup,keypress,scroll等。如果只绑定原生事件,不加以控制,会使得浏览器卡顿,用户体验差。为了提高js性能,建议在使用以上及类似事件的时候用函数节流或者函数去抖加以控制。

Underscore源码深度解析

四、underscore v1.7.0相关的源码剖析

1. _.throttle函数


_.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 &amp;&amp; options.leading === false) previous = now; 
// 这里引入了一个remaining的概念:还剩多长时间执行事件 
var remaining = wait - (now - previous); 
context = this; 
args = arguments; 
// remaining &lt;= 0 考虑到事件停止后重新触发或者 
// 正好相差wait的时候,这些情况下,会立即触发事件 
// remaining &gt; wait 没有考虑到相应场景 
// 因为now-previous永远都是正值,且不为0,那么 
// remaining就会一直比wait小,没有大于wait的情况 
// 估计是保险起见吧,这种情况也是立即执行 
if (remaining &lt;= 0 || remaining &gt; wait) 
{ 
if (timeout)
{ 
clearTimeout(timeout); 
timeout = null; 
} 
previous = now; 
result = func.apply(context, args); 
if (!timeout) context = args = null; 
// 是否跟踪 
} else if (!timeout &amp;&amp; options.trailing !== false)
{ 
timeout = setTimeout(later, remaining); 
} 
return result; 
};};

由上可见,underscore考虑了比较多的情况:options.leading:

第一次是否执行,默认为true,表示第一次会执行,传入{leading:false}则禁用第一次执行options.trailing:最后一次是否执行,默认为true,表示最后一次会执行,传入{trailing: false}表示最后一次不执行所谓第一次是否执行,是刚开始触发事件时,要不要先触发事件,如果要,则previous=0,remaining 为负值,则立即调用了函数所谓最后一次是否执行,是事件结束后,最后一次触发了此方法,如果要执行,则设置定时器,即事件结束以后还要在执行一次。remianing > wait 表示客户端时间被修改过。

2. _.debounce函数


_.debounce = function(func, wait, immediate) { 
// immediate默认为false 
var timeout, args, context, timestamp, result; 
var later = function() { 
// 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func 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(); 
// 第一次调用该方法时,且immediate为true,则调用func函数 
var callNow = immediate && !timeout; // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数 
if (!timeout) timeout = setTimeout(later, wait); 
if (callNow) { 
result = func.apply(context, args); 
context = args = null; 
} 
return result; 
};};

_.debounce实现的精彩之处我认为是通过递归启动计时器来代替通过调用clearTimeout来调整调用func函数的延时执行。

以上所述是小编给大家介绍的JavaScript性能优化之函数节流(throttle)与函数去抖(debounce),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章