JavaScript函数节流与去抖深度解析及实现方案
本文深入解析JavaScript中函数节流(throttle)与去抖(debounce)的核心概念、应用场景及底层实现原理。通过对比两种机制在高频事件处理中的差异,详细展示了从基础到完善的代码实现方案,并分析了不同实现策略对用户体验和性能的影响,帮助开发者根据具体场景选择最优方案。
引言
本文将深入探讨JavaScript中函数节流(throttle)与去抖(debounce)的基础概念,并对这两种技术进行详细分析。
基本概念解析
1、什么是节流和去抖?
节流(Throttle):形象地比喻为拧紧水龙头,让水流减少但并未完全停止。在现实生活中,接水时若不想一直等待,可离开片刻,待水快满时再回来。此时需控制水流速度,避免水满溢出造成浪费,这即是节流。在JavaScript中,典型场景包括图片懒加载监听页面滚动(scroll)事件或监听鼠标移动(mousemove)事件。由于这些事件在操作过程中会被浏览器频繁触发,导致处理函数执行频率过高,造成巨大的浏览器资源开销,甚至引发卡顿。虽然无法阻止浏览器触发事件,但可以通过节流降低处理函数的执行频率,从而减少资源消耗。
去抖(Debounce):该概念源于高中物理中的电路去抖。开关在闭合前可能产生抖动,导致灯泡闪烁甚至损坏。在Web页面中,类似情况存在于输入框的联想词查询场景。当用户快速输入时,频繁触发input事件并向后台发送请求,若网络延迟导致响应慢,联想词会频繁跳动,直到最后一个请求返回。去抖机制通过监听一定时间内的输入情况:若时间内无新输入,则判定输入完成并发送请求;若仍有输入,则视为用户仍在操作,不发送请求。
去抖与节流的区别在于:节流仅限制处理函数的执行频率,但仍会执行多次;而去抖则过滤掉中间的所有处理函数,仅在判定时间内最后一次事件触发时才执行。
JavaScript代码实现
2、JS实现。
经过前面的理论阐述,接下来我们将通过代码实践来具体实现节流和去抖功能。
节流:
/** 实现思路:
** 参数需要一个执行的频率,和一个对应的处理函数,
** 内部需要一个lastTime 变量记录上一次执行的时间
**/
function throttle (func, wait) {
let lastTime = null // 为了避免每次调用lastTime都被清空,利用js的闭包返回一个function确保不生命全局变量也可以
return function () {
let now = new Date()
// 如果上次执行的时间和这次触发的时间大于一个执行周期,则执行
if (now - lastTime - wait > 0) {
func()
lastTime = now
}
}
}
再看如何调用:
// 由于闭包的存在,调用会不一样
let throttleRun = throttle(() => {
console.log(123)
}, 400)
window.addEventListener('scroll', throttleRun)
此时若疯狂滚动页面,会发现每400ms打印一次“123”,而未加节流则会不断打印。你可以调整wait参数以体验不同效果。
然而,上述节流方法尚不完善,主要问题在于未获取事件发生时的this对象。此外,该方法简单粗暴地通过判断当前触发时间与上次执行时间的间隔来决定是否执行回调,这可能导致最后一次触发无法执行,或在用户触发间隔极短时无法执行,造成“误杀”。因此,需要对方法进行优化。
function throttle (func, wait) {
let lastTime = null
let timeout
return function () {
let context = this
let now = new Date()
// 如果上次执行的时间和这次触发的时间大于一个执行周期,则执行
if (now - lastTime - wait > 0) {
// 如果之前有了定时任务则清除
if (timeout) {
clearTimeout(timeout)
timeout = null
}
func.apply(context, arguments)
lastTime = now
} else if (!timeout) {
timeout = setTimeout(() => {
// 改变执行上下文环境
func.apply(context, arguments)
}, wait)
}
}
}
优化后的方法相对完善,调用方式与之前相同。
去抖:
去抖的实现思路与节流类似,但关键在于仅在抖动判定结束后,方法才会得到执行。
debounce (func, wait) {
let lastTime = null
let timeout
return function () {
let context = this
let now = new Date()
// 判定不是一次抖动
if (now - lastTime - wait > 0) {
setTimeout(() => {
func.apply(context, arguments)
}, wait)
} else {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
timeout = setTimeout(() => {
func.apply(context, arguments)
}, wait)
}
// 注意这里lastTime是上次的触发时间
lastTime = now
}
}
按照相同方式调用后,无论怎样疯狂滚动窗口,只有停止滚动时,对应的事件才会执行。
目前已有许多成熟的JavaScript库实现了去抖和节流,其大致思路基本一致。
其他实现方案对比
接下来分享网友提供的另一种实现代码:
方法一
1.这种实现方式的思路易于理解:设置一个间隔时间(如50毫秒),以此时间为基准设置定时器。当第一次触发事件到第二次触发事件的间隔小于50毫秒时,清除该定时器并设置新的定时器,依此类推,直到某次事件触发后50毫秒内无重复触发。代码如下:
function debounce(method){
clearTimeout(method.timer);
method.timer=setTimeout(function(){
method();
},50);
}
这种设计存在一个问题:原本应多次触发的事件,最终可能只执行一次。具体而言,在渐进式滚动事件中,若用户滚动速度过快或设置的节流间隔时间过长,最终滚动事件会表现为突然的跳跃,中间过程被节流截断。虽然此例略显夸张,但使用这种方式进行节流确实会让程序显得“更突兀”,严重影响用户体验。有一种弥补此缺陷的设计思路。
方法二
2.第二种实现方式与第一种略有不同:设置一个间隔时间(如50毫秒),以此时间为基准稳定分隔事件触发情况。即100毫秒内连续触发多次事件,也只会按照50毫秒一次的频率稳定执行。代码如下:
var oldTime=new Date().getTime();
var delay=50;
function throttle1(method){
var curTime=new Date().getTime();
if(curTime-oldTime>=delay){
oldTime=curTime;
method();
}
}
相比于第一种方法,第二种方法可能会执行更多次(有时意味着更多次后台请求,即消耗更多流量),但很好地解决了第一种方法清除中间过程的缺陷。因此,在具体场景中应根据实际情况择优选用。
对于方法二,这里再提供另一种具有相同功能的写法:
var timer=undefined,delay=50;
function throttle2(method){
if(timer){
return ;
}
method();
timer=setTimeout(function(){
timer=undefined;
},delay);
}
怎么优化 React 应用性能?避免不必要的重渲染
2026-11-05 12:43:40
怎么利用 Nuxt.js useHead 优化 SEO 与元数据管理
2026-11-05 10:37:58
JS混淆常见手段解析:字符串加密与控制流平坦化还原思路
2026-11-04 19:07:42
JavaScript能在浏览器中直接获取MAC地址吗
2026-11-03 20:34:17
JavaScript策略模式:用对象映射替代if-else分支
2026-11-03 19:46:11








