本文详细解析 JavaScript 中防抖(Debounce)与节流(Throttle)的核心原理及实现方式。通过自动门类比解释防抖逻辑,结合滚动统计等实际场景说明节流应用。文章深入剖析闭包在共享定时器中的作用,并完整展示处理 this 指向与参数传递的高级实现方案,帮助开发者有效优化高频事件触发的性能问题。
防抖
以自动门为例,当感应到有人靠近时,门会打开并启动 5 秒倒计时。若在 5 秒内有另一人靠近,门再次感应到人体,倒计时将重置为新的 5 秒。
在编程中,当事件触发时设置一个延迟执行时间。如果在延迟期间事件再次被触发,则重置延迟计时器,直到最后一次触发后的延迟时间结束,才执行最终动作。这种机制旨在防止事件被多次频繁触发。
在 Web 应用开发中,防抖技术常用于以下场景:
- 监听页面大小改变事件以进行数据统计。
- 监听页面滚动位置变化以进行统计或加载更多内容。
- 控制输入框连续输入时的请求频率,避免频繁发送请求。
初始阶段,点击按钮会执行 console.log('pay money') 操作。
<body>
<button id="btn">click</button>
</body>
<script>
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
}
btn.addEventListener('click', payMoney)
</script>
定义 debounce 函数,该函数接收目标函数作为参数,并返回一个新的函数。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
}
function debounce(func) {
// 在函数里面返回函数 , 只有当点击的时候才返回该函数
return function () {
func()
}
}
btn.addEventListener('click', debounce(payMoney))
在返回的函数内部设置延时器,指定延迟执行的时间。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
}
function debounce(func, delay) {
return function () {
setTimeout(_ => func(), delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
清除之前设置的延时器,确保未执行的旧任务被取消。
上述实现存在原因:
每次点击按钮时,都会执行返回函数内部的内容。
每次点击生成的执行函数都是独立的,彼此之间没有关联。
由于这些函数相互独立,清除延时器的操作无法作用于其他实例,因此在此阶段完全无效。
为了使这些独立的执行函数之间产生联系,需要利用作用域链(闭包)机制。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
}
function debounce(func, delay) {
return function () {
let timer
clearInterval(timer)
timer = setTimeout(_ => func(), delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
将 timer 变量定义在返回函数的外部,这样在定义监听事件的同时,就定义了 timer 变量。
得益于作用域链,所有独立的执行函数都能访问到这个外部的 timer 变量。
此时,timer 变量只被创建了一次,是唯一的。我们只是不断给 timer 赋值以重置延时,而不是创建新的变量。
每次清除延时实际上就是清除上一个定义的延时,相当于多个函数共用同一个外部变量。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
}
function debounce(func, delay) {
let timer
return function () {
clearInterval(timer)
timer = setTimeout(_ => func(), delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
此时的代码中,this 的指向是否为 window?
由于回调函数的执行上下文原因,运行时 this 已经指向了 Window 对象。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
console.log(this);
}
function debounce(func, delay) {
let timer
return function () {
clearInterval(timer)
timer = setTimeout(_ => func(), delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
针对 this 指向问题的解决办法:
在 setTimeout 之前将当前的 this 保存下来,此时 this 指向的是触发事件的按钮。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
console.log(this);
}
function debounce(func, delay) {
let timer
// 只有当点击的时候,才返回此函数,所以 this 是指向按钮的
return function () {
let context = this
clearInterval(timer)
timer = setTimeout(_ => {
func.apply(context)
}, delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
同时考虑参数传递的问题,添加 args 以保留原始参数。
const btn = document.getElementById('btn')
function payMoney() {
console.log('pay money');
console.log(this);
}
function debounce(func, delay) {
let timer
return function () {
let context = this
let args = arguments
clearInterval(timer)
timer = setTimeout(_ => {
func.apply(context)
console.log(context, args);
}, delay)
}
}
btn.addEventListener('click', debounce(payMoney, 1000))
节流
在事件第一次触发后立即执行,随后防止接下来多次触发,通过时间间隔限制来控制执行频率。
滚动屏幕场景:统计用户滚动屏幕的行为以作出相应的网页反应。
当用户不断进行滚动操作时,会不断产生请求,导致网络请求数量激增,容易引发 ⛔️ 网络阻塞。
我们可以在触发事件时立即执行任务,然后设定一个时间间隔。在这段时间内,无论用户如何滚动都忽略后续操作。
当时间间隔结束后,如果监测到用户仍有滚动行为,则再次执行任务,并重新设置时间间隔。
首先,编写一个在改变页面尺寸时同时改变页面背景颜色的代码示例。
function coloring() {
let r = Math.floor(Math.random() * 255)
let g = Math.floor(Math.random() * 255)
let b = Math.floor(Math.random() * 255)
document.body.style.background = `rgb(${r}, ${g}, ${b})`
}
window.addEventListener('resize', coloring)
function throttle(func, delay) {
let timer
return function () {
timer = setTimeout(_ => {
func()
}, delay)
}
}
window.addEventListener('resize', throttle(coloring, 2000))
判断当前触发的事件是否处于设定的时间间隔内。
- 如果不在间隔内:触发事件并执行任务。
- 如果在间隔内:不触发事件,忽略操作。
function throttle(func, delay) {
let timer
return function () {
// timer 被赋值了,直接返回,即不执行任务
if (timer) {
return
}
// 此时 timer 没被赋值,或 timer 已经执行完了
// 为 timer 赋值进行延时执行
timer = setTimeout(_ => {
func()
// 延迟执行以后我们要清空timer的值
timer = null
}, delay)
}
}
window.addEventListener('resize', throttle(coloring, 2000))
解决 this 指向问题(虽然当前示例中 this 指向 Window,但需确保通用性)。
function throttle(func, delay) {
let timer
return function () {
let context = this
let args = arguments
// timer 被赋值了,直接返回,即不执行任务
if (timer) {
return
}
// 此时 timer 没被赋值,或 timer 已经执行完了
// 为 timer 赋值进行延时执行
timer = setTimeout(_ => {
func.apply(context, args)
// 延迟执行以后我们要清空timer的值
timer = null
}, delay)
}
}
window.addEventListener('resize', throttle(coloring, 1000))
节流的核心在于控制事件间隔。另一种常见的时间间隔判断方法是使用 Date 对象。
function throttle(func, delay) {
// 我们要和前一个时间点进行比较才能确定是否已经过了时间间隔
// 在返回函数外围,避免每次执行都被自动修改
let pre = 0
return function () {
// 保存执行函数当时的时间
let now = new Date()
// 刚开始,一定会执行
if (now - pre > delay) {
// 已经过了时间间隔,就可以执行函数了
func()
// 执行后,重新设置间隔点
pre = now
}
}
}
window.addEventListener('resize', throttle(coloring, 1000))
解决参数传递问题,确保原始函数的参数能被正确传递。
function throttle(func, delay) {
let pre = 0
return function () {
let context = this
let args = arguments
let now = new Date()
if (now - pre > delay) {
func.apply(context, args)
pre = now
}
}
}
window.addEventListener('resize', throttle(coloring, 1000))
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!