揭秘「防抖」技术,让每一次交互都恰到好处
前言
在数字化洪流中,每一次点击、滚动、输入,都是对系统性能的考验。如何在海量交互中保持冷静,不让服务器心跳加速?答案就是——防抖技术!
正文
防抖的概念
防抖主要用于处理那些会触发大量、高频事件的操作,比如鼠标点击、窗口大小改变、输入框输入等。其核心思想是,在规定的时间内,如果某个事件被连续触发多次,只执行最后一次,前面的所有请求都会被忽略,直到规定时间结束后,才会执行一次。 车
防抖的基本原理
防抖的核心思想是:当第一次触发事件时,设置一个定时器,如果在这段时间内再次触发事件,则取消之前的定时器并重新设置一个新的定时器。只有在最后一次触发事件之后的一段时间内没有新的触发,才会真正执行函数
防抖的使用
我们没有防抖的时候,如果一直点击下面的提交按钮,按钮就会被一直触发,很影响性能(我们假设输出内容是个ajax请求)
<body>
<button id="btn">提交</button>
<script>
var btn = document.getElementById('btn');
btn.addEventListener('click', () => {
//ajax请求
console.log('提交');
})
</script>
</body>
</html>
像这样:
如果我们加上定时器:
let btn = document.getElementById('btn');
//不行,点击几次就有几次计时函数
btn.addEventListener('click', function() {
//ajax请求
setTimeout(() => {
console.log('提交');
}, 1000)
})
还是不行,只是提交的打印显示会延迟一秒出现。
现在我们就需要一个防抖函数
极简版
function debounce(fn) {
let timer = null;
//这是一个闭包(一个函数的内部函数被返回到函数外部去调用)
return function () {
//如果第二次的时间没到1s,就销毁上一次的计时器
clearTimeout(timer)
timer = setTimeout(fn, 1000)
}
}
这段代码的执行机制可以通过一个暴力点的比方理解:
有两个人,一个A一个B,AB起了争执,不想听B废话,B说你听我解释,A就给B一巴掌让它闭嘴( clearTimeout(timer)
)打断施法,B被打了还要说,直到A不打了,B才能完整解释
(防抖函数是A,要执行的函数是B)
上面这段debounce
函数实现是可行的,但为了更完整和实用,我们通常还会在防抖函数内部保存 this
上下文和参数,以便在最终调用原始函数时能够正确地执行。下面来介绍更完整的版本:
完整版
在介绍之前我们要先了解一下下列代码中第4行的this
,
var btn = document.getElementById('btn');
function handle() {
console.log('提交', this);
}
//点击一下就执行一次返回函数
btn.addEventListener('click', handle)
//防抖函数
function debounce(fn) {
let timer = null;
//这是一个闭包
return function () {
//如果第二次的时间没到一秒就销毁上一次的计时器
clearTimeout(timer)
timer = setTimeout(fn, 1000)
}
}
this
是handle()
函数的,但是被addEventListener
干扰了,导致this
不指向全局window而指向button
DOM元素
在 JavaScript 中,函数的
this
值取决于函数是如何被调用的,而不是它在哪里被定义的。当函数作为事件处理函数被调用时,this
的值通常指向触发事件的 DOM 元素。
而handle
不是独立调用的,是addEventListener
触发的,所以this不指向window
我们加上防抖,把第7行改成:
btn.addEventListener('click', debounce(handle))
再执行:
发现现在的this
指向了window
为啥呢??
现在的handle()
是debounce()
里的定时器调用的,而定时器里的回调函数的this
默认指向window
那我们是希望这个this指向button
的,防抖函数只防抖就行。
定时器里的this
指向全局,但是debounce()
里的this还是指向debounce()
,所以我们可以通过使用 fn.call(this)
来显式地绑定 fn
函数的 this
值
function debounce(fn) {
let timer = null;
return function () {
const that = this
clearTimeout(timer)
timer = setTimeout(function() {
fn.call(that)
}, 1000)
}
}
带参数版
任何一个事件在触发的时候都会接收一个事件对象作为参数
这个对象包含了有关事件的所有信息,比如事件的类型、触发事件的目标元素、事件的坐标、按键状态等等
所以handle()
也会带一个参数e
function handle(e) {
console.log('提交', e);
}
在不防抖的情况下,执行结果:
那如果咱们加上防抖呢?
得,又搞丢了一个东西,因为事件对象 e
在外层被忽略了,所以 handle
函数无法接收到它。
function debounce(fn) {
let timer = null;
return function (e) {
const that = this
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(that,e)
}, 1000)
}
}
第8行代码通过在返回的函数中接受参数 e
,并在内部的 setTimeout
回调中使用 fn.call(that, e)
,确保事件对象能够被正确地传递给最终的事件处理器函数 fn
总结
- 形成闭包:
debounce
返回一个内部函数,该函数可以访问debounce
的局部变量,如计时器。 - 管理计时器: 内部函数每次调用时清除现有计时器并设置新计时器,确保只在最后调用后延迟执行。
- 保持
this
上下文: 保存并还原原始函数的this
指向,确保在正确上下文中执行。 - 传递参数: 确保原始函数可以接收到所有调用时传递的参数。
以上就是本篇文章全部内容,防抖技术不仅是一套编程技巧,更是前端工程师在用户体验与性能优化之间找到的平衡点。希望本文对你有所帮助,感谢你的阅读!
转载自:https://juejin.cn/post/7389925417787539493