防抖
防抖是什么
举个例子:
这段代码是一个简单的HTML页面,其主要功能是在一个高度为200像素的蓝色矩形容器中显示一个数字,并且每次鼠标移动在容器内时,数字会递增。
当用户使用鼠标或触摸屏时,可能会出现一个问题:尽管他们只是轻轻移动了一下鼠标或手指,但屏幕上的数字却在飞速增加。这种现象就像我们的手在微微颤动,却造成了屏幕上的数字剧烈变化一样。这个问题会导致用户感觉操作不准确,降低了用户体验。为了解决这个问题,就有"防抖"这个概念。
而且当事件处理函数不再仅仅执行简单的+1操作时,例如涉及到耗时的任务>触发频率的情况,会导致页面性能下降,甚至出现卡顿现象。这是因为在短时间内频繁触发事件会导致浏览器执行大量的JavaScript代码,占用了大量的CPU资源,影响了页面的响应速度和用户体验。为了解决这个问题,可以使用"防抖"技术来优化前端性能。
防抖的作用
防抖的核心思想:在事件被触发后,延迟一定时间再执行事件处理函数,如果在这段时间内又触发了相同的事件,则销毁上一次,重新计时。
防抖带来的效益:
- 减少不必要的事件触发:在用户执行某些高频率操作时(例如窗口大小调整、滚动、键盘输入等),防抖可以确保在特定时间段内事件只会被触发一次,从而减少多次触发带来的资源浪费。
- 提高性能:频繁触发事件会导致程序反复执行一些计算密集型操作,如重新渲染页面、发送网络请求等。防抖技术通过减少事件的触发频率,降低了CPU和内存的占用,从而提高程序的整体性能。
- 改善用户体验:过于频繁的响应用户操作可能会导致界面卡顿或反应迟缓,影响用户体验。防抖技术可以使应用程序的响应更加平滑和稳定,提升用户满意度。
防抖实现原理
防抖的工作原理:设置一个定时器,当事件触发时,启动定时器。如果在定时器到期之前,事件再次触发,则重置定时器。只有在定时器到期后,事件处理函数才会被实际执行。
所以针对上面的例子js应该写成这样:
简易版:
var count = 1;
var container = document.getElementById('container');
// 事件处理函数
function getUserAction() {
container.innerHTML = count++;
}
//防抖函数
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
// 这里的func函数里的this -> 作为函数独立调用,所以this -> window
timer = setTimeout(func, delay);
};
}
container.onmousemove = debounce(getUserAction, 1000); // 在mousemove事件上应用防抖
上述代码可以实现一个简单的防抖,但是整体性能不够健壮,存在一些问题:
- 上下文(Context)的丢失问题:
getUserAction
函数在被调用时,可能会丢失其原始的上下文(即this
指向)。 - 传递参数问题:
getUserAction
函数无法接收到事件参数。
进阶版:
var count = 1;
var container = document.getElementById('container');
// 事件处理函数
function getUserAction() {
container.innerHTML = count++;
}
function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
// 这个函数里的this 指向调用他的对象 也就是container
const context = this;
const args = arguments;
timer = setTimeout(function() {
func.apply(context, ...args);//
}, delay);
};
}
container.onmousemove = debounce(getUserAction, 1000); // 在mousemove事件上应用防抖
-
在进阶版中,通过保存当前的上下文(
context
),并使用func.apply(context, ...args)
来调用原始函数,确保了getUserAction
函数在执行时依然具有正确的上下文。 -
在进阶版中,通过保存
arguments
,并在调用原始函数时使用...args
来传递参数,确保了getUserAction
函数能够正确接收到事件参数。
timer是什么
timer 是一个用于存储定时器 ID 的变量。具体来说,timer 的数据类型是 number
(在浏览器环境下),它表示一个由 setTimeout
返回的标识符,可以用来控制延时操作。
timer 的作用 :
-
存储定时器 ID:timer 存储 setTimeout 的返回值,用于唯一标识这个定时器。每次调用 debounce 返回的函数时,都会重新设置一个定时器,并将新的定时器 ID 赋值给 timer。
-
清除之前的定时器:调用 clearTimeout(timer) 可以取消先前设置的定时器。如果在 delay 时间内再次触发事件,这个定时器会被清除,从而避免调用 func。
-
延时执行函数:在事件触发时,设置一个新的定时器,延时执行传入的函数 func。只有在 delay 时间段内没有再次触发事件时,定时器到期,才会调用 func。
为什么改变this指向
this指向问题可以看看这篇文章:juejin.cn/post/736660…
-
因为事件处理函数 getUserAction 需要正确的 this 引用来操作
container
元素的 innerHTML 属性。如果不处理好 this,在防抖函数中调用 getUserAction 时,可能会丢失正确的上下文,从而导致错误。 -
apply 方法
允许我们显式地设置函数执行时的 this 值和参数。通过 apply 方法,我们可以确保防抖函数在调用原始函数 func 时,能够使用正确的 this 值和参数!
为什么说这种方式使用了闭包
闭包是指函数能够记住并访问其词法作用域,即使当该函数在其词法作用域之外执行时。闭包使得内部函数可以访问外部函数的变量和参数。
当调用 debounce
函数时,timer
被创建并初始化。然后,debounce
返回一个新的函数。这个新函数就是一个闭包,因为它捕获了 debounce
的词法环境,包括 timer
变量。
即使 debounce
函数执行完毕并返回,timer
变量依然存在于返回的新函数的闭包中。当调用返回的新函数时,它可以访问和修改 timer
。
上面进阶版的"防抖"已经满足了合格要求,但是那只是一般的的防抖,那什么才是让大厂面试官眼前一亮的"二班"防抖呢?我们来看看
"二班"防抖
-
立即执行选项:有时候,我们可能希望防抖函数在第一次触发时立即执行,而不是等待延迟时间。为此,可以添加一个选项
immediate
来控制是否立即执行函数 -
返回值:这个防抖函数目前没有返回任何值。如果需要返回原始函数的执行结果,可以将其保存下来并在适当的时候返回。
所以为了满足这些需求。"二班"防抖应该这样写:
function debounce(func,delay, immediate) {
//自由变量空间
var timer, result
//真正执行的函数
return function () {
// 二传手
var context = this
var args = arguments
if (timer) clearTimeout(timer);
if (immediate) {
var callNow = !timer
//为了一段时间时后下一次触发时能够继续立即执行第一次
//为实现从一段时间内只执行最后一次——>从一段时间内只执行第一次
timer = setTimeout(function () {
timer = null //垃圾回收机制
}, delay)
if (callNow) result = func.apply(context, args)
} else {
timeOut = setTimeout(function () {
result = func.apply(context, args)
}, delay)
}
return result;
}
}
这段代码中的 debounce
函数接受三个参数:func
是要执行的函数,delay
是延迟的时间(毫秒),immediate
是一个布尔值,表示是否立即执行函数。函数内部定义了一个 timer
变量和一个 result
变量,用来存储定时器和函数执行的结果。
当调用 debounce
返回的函数时,会判断 immediate
是否为 true
,如果是,会立即执行一次函数并设置一个定时器,在延迟时间结束后将定时器设为 null
。
为什么在延迟时间结束后将定时器设为 null
:
-
延迟重置
timer
: 这个定时器在delay
时间后执行,并将timer
置为null
。这样做是为了在这段时间内防止立即执行(immediate
)的功能被重复触发。如果在delay
时间内再次调用防抖函数,因为timer
仍然存在并未被置为null
,因此不会立即执行,而是等待delay
时间后再执行。 -
实现防抖的效果: 在
immediate
模式下,当第一次调用防抖函数时,timer
为null
,因此会立即执行并设置一个新的定时器。在delay
时间内,如果再次调用防抖函数,由于timer
仍然有效,会重置定时器并不会立即执行。只有当delay
时间结束后,timer
被置为null
,下一次调用防抖函数时才会再次立即执行。这确保了在delay
时间内只会有一次立即执行,而不是每次触发都立即执行。
让我们具体看一下 immediate
为 true
时的工作流程:
-
第一次调用防抖函数:
-
timer为 null。
-
callNow 被设置为 true(因为 !timer 为 true)。
-
执行函数 func 并设置 timer:
timer = setTimeout(function () { timer = null; }, delay);
-
-
在
delay
时间内再次调用防抖函数:-
timer 不为 null。
-
清除现有的定时器 clearTimeout(timer) 并设置一个新的定时器:
timer = setTimeout(function () { timer = null; }, delay);
-
callNow被设置为 false(因为 !timer 为 false)。
-
因此不会立即执行 func。
-
-
经过
delay
时间后:- 定时器执行,将 timer 置为 null。
-
再次调用防抖函数:
- timer 为 null。
- callNow 被设置为 true(因为 !timer 为 true)。
- 再次立即执行函数 func。
总结起来,这个 setTimeout
操作的关键作用是确保在 delay
时间内,函数只能立即执行一次(如果 immediate
为 true
),并在 delay
时间后重置 timer
以便下一次调用可以再次立即执行。
最后
如果你学会了"二班"防抖,那么咱们就不是一般人了!