前言:当用户点击变成"连点狂魔"——前端性能的隐形杀手
在数字化体验至上的今天,用户对交互的即时反馈有着近乎苛刻的要求。想象这样一个场景:用户正在填写一个至关重要的表单,当手指最后一次敲击回车键或点击提交按钮时,网络请求的"转圈圈"却让页面陷入了短暂的"假死状态"。焦虑的用户开始化身"连点狂魔",疯狂点击按钮,而你的服务器却在瞬间收到数十个重复请求——这不仅是用户体验的灾难,更是后端系统的噩梦。
这种看似无害的用户行为,实则暗藏三大危机:
- 服务器过载:瞬间并发请求可能击穿服务防线
- 数据混乱:重复提交导致数据库出现脏数据
- 体验崩塌:用户看到混乱的反馈陷入更深的焦虑
面对这场"点击风暴",前端工程师需要两把利剑:防抖(Debounce) 与节流(Throttle) 。它们就像数字世界的交通警察,一个负责在拥堵路口设置"冷静期",一个在繁忙路段安装"红绿灯",共同守护着应用性能的黄金准则。本文将带您深入解析这两种技术的实现原理,通过代码解构和场景模拟,让您掌握应对高频交互的终极解决方案。
场景
比如:
- 一个表单提交页面,用户输入各种用户信息后,点击按钮,但是因为网络延迟可能导致一定时间内没有响应,用户不知道问题所在可能会多次点击按钮,带来高并发,增加服务器的压力
没有意义的请求
- 提交按钮---点击按钮后,立即禁用按钮,防止多次点击
防抖
- 在规定时间内,没有新的触发,才执行(简单来说就是用户手抖一直点击,为了防止触发过多次数)
- 借助闭包保存上一次的定时器
- 每次触发都清除上一次的定时器,重新设置定时器
- 考虑参数的问题
- 考虑this的指向问题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
function handle(e) {
console.log(e);
console.log(this);//this默认指向add前面dom元素, 但是这里改变 是被独立调用的触发默认绑定规则,所以指向widnow 所以显示绑定
}
function debounce(fn, wait) {
let timer = null; //timer放到闭包里面
return function (...arg) {
//e改成...arg 把所有参数接收,转变为数组
// fn()不能立即触发,倒计时
clearTimeout(timer);
timer = setTimeout(() => {
fn(...arg); //这里是解构数组 显示绑定
}, wait);
};
}
// 哪一个函数体在这之后被调用,那个函数就有e参数
btn.addEventListener("click", debounce(handle, 1000)); //handle是由点击触发来调用的,所以这里仿函数体
</script>
</body>
</html>
这里防抖的实现是通过闭包来实现的,将定时器setTimeout写成表达式,赋值给timer,将timer放到return funtion的外面,闭包简单来说就是在一个函数体里返回一个函数体,在第一层函数体外使用内层函数,v8会自动的保存内部函数所需要的变量到一个集合中,这个集合就叫做闭包。
闭包的出现基于v8俩条铁律,内部作用域一定能访问外部作用域,而且但函数执行完毕,所占内存空间一定会被销毁,为了同时满足两条规则,创建了闭包来保存函数销毁后,内部函数所需要的变量。
一开始timer值为null,在返回内部函数后,外层函数所占空间被销毁,而timer则会被保存到闭包中,此时timer值为null,在内部函数执行后,闭包里timer的值会被修改为一个新的定时器,定时器开始执行。
与此同时,如果有人在定时器时间未结束之前再次点击按钮,触发绑定事件的话,防抖函数再次触发,导致闭包空间内的timer值会被再次赋值为新的定时器,因此只要有人一直点击按钮触发事件,定时器就会一直刷新,直到不再点击。
有人会问,外层不是有timer=null吗,不应该每次都将闭包里面的timer值先修改为null再对它赋值为定时器吗?
第一次开始的时候闭包是空的,会自动保存内部函数所需要的变量,将闭包里timer赋值为null,在内部函数调用后,会将闭包改为定时器,在第二次点击(且第一次定时器未走完时,timer定时器仍然在闭包中保存)后,内部函数会直接去闭包中查找而不是先创建timer赋值为null,再赋值为定时器。
如果像这样写无异于将timer放到内部函数里面,每次执行内部函数,都会创建一个新的定时器,timer先赋值为再赋值为定时器,点击多少次就创建多少个防抖函数,就没有起到防抖的作用了。
闭包本质就是提供了一个保存内部函数所需变量的存储空间,不会随着外部函数执行完毕而销毁。
当然你也可以直接将timer定义到全局环境当中,但缺点是如果代码非常多,是不是每次写一个函数要保存内部变量都需要全局变量呢?
显然将变量封装到函数里面,通过闭包保存显得更加优雅。
- 考虑参数的问题
- 考虑this的指向问题
这里我们还需要考虑的俩个问题是参数于this指向
事件绑定里面的函数 默认会有一个事件对象e,但是这里执行的函数不再是handle,而是一个匿名函数,所以handle不再有事件对象e,要通过...arg将所有参数传入handle中,包括原来的事件对象,这里的...args是对象解构
原来handle函数里面this是指向按钮的,但是这里加了一个防抖函数之后,handle是被独立调用的于是里面的this触发默认绑定规则,指向window全局,因此为了解决这个问题,我们需要使用显示绑定规则call()方法改变,handle里面的this的指向,将handle里面的this绑定指向外部函数的this,也就是外部函数debounce的this.为什么不是这的箭头函数呢? 因为call显示绑定this是指向外层函数的(箭头函数不算)
防抖一定时间点击次数过多,就会一直刷新定时器,一直点一直刷新。
截流
- 有节制的执行:在一定时间内,只执行一次
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="btn">确认</button>
<script>
function handle(e) {
console.log("像后端发送请求");
console.log(this);
}
let btn = document.getElementById("btn");
btn.addEventListener("click", throttle(handle, 2000));
function throttle(fn,wait) {
let preTime = null;
return function (...arg) {
nowTime = Date.now(); //第一次点击的时间戳
if(nowTime-preTime>=wait)
fn.call(this,...arg);
preTime = nowTime;
};
}
</script>
</body>
</html>
这段代码指定规定时间内执行规定次数。
结语:让交互更有"节奏感"——前端性能的艺术平衡
防抖与节流,看似是技术细节的优化,实则是用户体验的精心编排。通过本文的深度解析,我们不仅掌握了两种技术的实现精髓,更领悟了前端性能优化的核心哲学——在资源消耗与用户体验之间找到完美的平衡点。
防抖技术教会我们:有时候,适当的"延迟满足"能创造更大的价值。就像咖啡机需要时间萃取精华,关键操作也需要等待最佳时机。通过闭包管理定时器、精准控制this指向和参数传递,我们为系统设置了"冷静期",让每个操作都经过深思熟虑。
节流技术则展示了:有节奏的释放比无序的爆发更可持续。如同城市交通需要红绿灯调节车流,高频率事件也需要设置"通行配额"。通过时间戳差值计算,我们为应用装上了"智能节流阀",确保关键操作在合理频率内有序执行。
在实际开发中,这两种技术往往需要组合使用:防抖处理最终状态,节流控制过程变化。就像指挥家需要同时掌控乐章的强弱快慢,优秀的前端工程师也要懂得在不同场景切换技术方案。
欢迎友友点赞评论,如有错误欢迎指正