引言:为什么防抖值千金?
当你信心满满走进大厂面试间,面试官突然甩出:"手写一个防抖函数"——此刻多少人的笑容僵在脸上?防抖作为JS高频考点,早已成为区分"前端搬砖工"和"工程师"的试金石。掌握它,offer就在眼前;不会它?出门右转慢走不送。
一、致命场景:疯狂点击的毁灭力量
想象一个用户信息提交页面:
<button id="btn">提交订单</button>
<script>
document.getElementById('btn').addEventListener('click', () => {
console.log("💰 扣款请求已发送!");
// 实际场景:向服务器发送支付请求
});
</script>
当用户点击提交后:
1️⃣ 网络延迟导致页面无响应
2️⃣ 用户焦虑狂点10次提交按钮
3️⃣ 服务器收到10个重复支付请求
4️⃣ 用户投诉 + 财务对账崩溃 = 程序员祭天
二、青铜方案:禁用按钮大法
初级解决方案简单粗暴:当用户点击提交按钮时,立即禁用提交按钮,可防止用户提交重复的请求
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
btn.disabled = true; // 立即锁定按钮
console.log("🚫 按钮已禁用,请求发送中...");
// 发送请求...
});
</script>
禁用按钮大法 虽然简单直接且能够防止手抖党,但是这个方法有以下几个致命的缺陷
- 请求失败后按钮永远被锁(需额外重置逻辑)
- 用户体验极差(用户怀疑系统卡死)
- 无法解决滚动条/输入框等非按钮场景
三、王者方案:防抖技术登场
防抖核心哲学:
在事件风暴中,只执行最后一次真正的呼唤
什么是 防抖 呢? 防抖 是一种优化手段,用于控制高频事件的触发频率。其核心思想是,在规定的时间内如果事件被重复触发,则之前的触发会被取消,只有最后一次触发会在设定的时间间隔后执行。例如,在搜索框输入时,用户快速连续输入字符,每次输入都可能触发请求,但使用防抖可以避免频繁请求服务器,仅在用户停止输入一定时间后才执行搜索操作。
手搓防抖的绝句诗
首联:闭包藏器待时发(闭包保存定时器,等待时机执行)
为什么要用 闭包 来保持定时器呢?
在V8引擎中,代码执行前需先进行编译,形成全局执行上下文,随后触发函数的执行上下文。这一过程涉及创建执行上下文对象、查找行参和变量声明、统一行参和实参等步骤。函数声明和定义在特定上下文中,但可能不在同一上下文中执行。执行完毕后,执行上下文会被销毁,以避免内存溢出,保证引擎运行效率。此外V8引擎还有一条规则,根据作用域查找规则--即内部作用域能够访问外部作用域。
<script>
function debounce(fn,wait){
var timer = null //闭包藏定时器
return function(){
clearTimeout(timer)
timer = setTimeout(()=>{
fn()
},wait)
}
}
</script>
在两条规则下,debounce(fn,wait) 首先进行编译,编译完成后,执行上下文被销毁,用户点击鼠标后执行 function() ,进行预编译时,执行 clearTimeout(timer) 由于内部作用域没有 timer,则会向外作用域找,但是外作用域即 debounce(fn,wait) 的执行上下文已经被销毁。可以看出内部作用域访问外部作用域变量的规则,以及函数执行完毕后执行上下文的销毁机制的这两条规则互相冲突,所以引擎会保留一个小空间(即 闭包空间 )来存储内部函数需要访问的外部变量,以确保代码的正确运行。
颔联:破旧立新延火华(清除旧的定时器,再设新的,延迟执行如延绵的火花)
用户点击一次提交,会触发一次 function(),然后执行 clearTimeout(timer),在内部作用域找不到就会去外部作用域即function()找,但是由于执行上下文被销毁,所以只能去闭包里找,此时timer = null。 代码接着执行此时闭包中 timer = set ,当用户在 wait 中再次点击提交,又会触发一次 function(),然后执行clearTimeout(timer),在内部作用域找不到就会去外部作用域即function()找,但是由于执行上下文被销毁,所以只能去闭包里找,此时 timer = set ,执行clearTimeout(timer)将其销毁,相对于timer = null,然后执行下一行代码此时 timer = set ,所以用户在 wait 时间内点击提交 ,会将上一个旧的定时器销毁 ,然后设置新的定时器 ,这样就达到只执行最后一句呼唤 ,达到防抖效果。
颈联:传音入密参无差(参数传递要准确无误)
在没有启用防抖技术时,事件处理函数可以直接接受事件参数,事件参数包含了与事件相关的所有信息,比如事件类型、触发事件的元素、事件发生时的状态(如鼠标位置、按键状态等)等。但是在启用防抖技术后,原始事件参数面临三重封锁:
- 屏障隔离:防抖包装函数截获事件对象
- 时间断层:延迟执行导致参数时效性丢失
- 通道变形:异步回调改变参数传递路径
在使用防抖技术前,handle(e)拥有实践参数,使用防抖技术后,变成function()拥有事件参数,所有可以通过以下代码来保证handle(e)的参数传递。
<script>
let btn = document.getElementById('btn')
function handle(e){
// console.log(e.x,e.y)
console.log('向后端发请求')
}
btn.addEventListener('click',debounce(handle,1000))
function debounce(fn,wait){
var timer = null
return function(...arg){ //保证参数传递
clearTimeout(timer)
timer = setTimeout(()=>{
fn(...arg) //保证参数传递
,wait)
}
}
</script>
尾联:定海神针指向家(this指向要固定,如同定海神针指向正确的家)
this 的指向:
- 函数被独立调用, this 指向Window
- 函数被对象调用, this 指向这个对象
- 函数被显示调用, this 指向显示绑定的对象
- 函数被 new 调用, this 指向实例对象
在使用防抖技术之前, this 指向 btn 。
<script>
let btn = document.getElementById('btn')
function handle(e){
console.log('向后端发请求',this)
}
btn.addEventListener('click',handle)
</scrip >
在使用防抖技术后,function() 的 this 指向 btn 。handle() 被 fn() 独立调用,此时 fn() 的 this 指向 Window ,要保持 fn() 的 this 指向 btn ,则用显示绑定:将 fn() 的 this 同步 function() 的 this :将 fn() 的 this 同步 function ()的 this ,此时 fn() 的 this 指向 btn 指向正确。
<script>
let btn = document.getElementById('btn')
function handle(e){
// console.log(e.x,e.y)
console.log('向后端发请求')
}
btn.addEventListener('click',debounce(handle,1000))
function debounce(fn,wait){
var timer = null
return function(...arg){
clearTimeout(timer)
timer = setTimeout(()=>{
fn.call(this,...arg) //显示绑定:将fn()的this同步外一层的this即function()的this
},wait)
}
}
</script>
但是由于计数器中的箭头函数不存在 this ,当计数器中不是箭头函数时 ,当 fn.call(this,...arg) 此时 fn() 的 this 与 计数器中 function() 的 this 同步 ,此时同计数器指向 Window ,所以为保证 fn() 的 this 指向 btn ,在 function() 中引入 that ,巧妙的越过了计数器中 function() 的 this 指向,使 fn() 的 that 同步 function() 的 that 指向 btn ,如下方代码所示:
<script>
let btn = document.getElementById('btn')
function handle(e){
console.log('向后端发请求')
}
btn.addEventListener('click',debounce(handle,1000))
function debounce(fn,wait){
var timer = null
return function(...arg){
let that = this //使that指向btn
clearTimeout(timer)
timer = setTimeout(function(){
fn.call(that,...arg) //使fn()的that与function()的that同步,指向btn
},wait)
}
}
</script>
四、防抖的胞弟:节流(Throttle)
面试官可能追加考题:"那节流呢?"——展现全面实力的时刻到了!
节流 : 有节制的执行:在一定时间内,只执行一次
代码如下:
<script>
let btn =document.getElementById('btn')
function handle(){
console.log('向后端发生请求');
}
btn.addEventListener('click',throttle(handle,2000)) //每两秒执行一次
function throttle(fn,wait){
let preTime = 0 //用闭包保留上一次执行的时间戳
return function(...arg){
let nowTime = Date.now() //第一次点击的时间戳
if(nowTime - preTime > wait){
preTime = nowTime
fn.call(this,...arg)
}
}
}
</script>
总结:防抖在手,Offer我有!
防抖技术就像一位机智的交通警察:当疯狂点击的车流(高频事件)试图涌入服务器这座脆弱的小桥时,警察举起延迟执行的指挥棒喊道:"都给我排队!最后那辆车过,其他统统取消!"——闭包是它的执勤亭(藏定时器),参数传递是它的对讲机(精准通讯),this指向是它的警徽(锁定目标),而节流则是它的孪生兄弟,改成了"每分钟放行10辆车"的限流方案。掌握这套"事件交通管制系统",你就能让代码在面试官面前优雅通行,直通大厂收费站!