用户在浏览网页的时候,总会不断与浏览器页面产生交互,产生各种与UI相关的事件,比如点击、按键、滚动,甚至包括动画以及DOM变动,等等。在产生这些交互的时候,必然会占用浏览器的一些资源,倘若某些事件触发过多,必然会对页面性能产生一定影响,因此我们就需要限制这些事件出发的次数。这不,防抖与节流就来了~
防抖
是什么?
首先,我们来思考个问题,不知你在浏览网页的时候是否有这样的体验:在页面中点击某个按钮,手一抖,就不慎点了2次,与按钮对应的事件也就触发了2次😏
这种情况常发生在用户提交表单时手抖多点了几下,或是用户网络不太好点了一次又点了多次,或是别有用心的用户疯狂点击提交按钮(比如直接执行元素的.click()
函数),这些情况造成了表单被疯狂提交,服务器就收到了一堆垃圾请求(重复数据),此时网络就被拥堵了。
都怪你的手,为什么要抖🌚
因此,为了防止你的手抖,而造成这种情况发生,我们就要对这个按钮的事件处理函数进行一些处理,也就是这里的防抖。
原理?
防抖,实际上就是在触发一个事件以后,在之后的一段时间里,事件处理函数只能被执行一次。
嗯,首先我们应该设置一个超时的时间范围 —— 在触发一个事件以后,开始计时,在超时时间未达到之前,若再次触发同一事件,则重新计算超时时间,再次触发则再次重新计算,一直延后(只要超时时间不到,就不会触发事件处理函数);超时时间到达时,如果同一事件没有再次触发,则将执行事件处理函数。
试试?
<body>
<h1 id="h1">点我有惊喜</h1>
<script>
function clickHandler(e) {
console.log(e)
}
h1.addEventListener('click', clickHandler)
</script>
</body>
这是一个简单的代码片段,用户在点击h1元素的时候,会触发点击事件的处理函数。此处没有做任何处理,用户无论在多短时间内点击了多少次,都会执行处理函数。因此,我们需要尝试优化。
第一步,加定时器。
let timer = null
function clickHandler(e) {
timer = setTimeout(() => {
console.log(e)
}, 800)
}
h1.addEventListener('click', clickHandler)
此处没什么卵用,只是使得点击后的函数能够延迟800ms执行,但最终还是点多少次执行多少次
第二步,在某个地方重新计时。
let timer = null
function clickHandler(e) {
// 清除定时器
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
console.log(e)
}, 800)
}
h1.addEventListener('click', clickHandler)
重点来了,我们在设置新定时器之前,先看看之前是否有定时器,如果有了,我们就把之前的定时器清除。经过测试,在800ms内不断点击h1后,事件处理函数将不再会点几次执行多少次,我们的预期目标已经达成。
好了,理论来来说,节流大概就是这么个东西吧,在我看来,它最大的作用是解决了人的手抖问题~
与业务分离
但我们有木有可能对上面这个函数来进行改造,写一个通用函数,使得我们能够更加得心应手地对任意函数也进行节流呢?
既然是对一个函数进行处理,因此在编写这个通用函数的时候我们需要接收一个要处理的函数fn
作为参数,并返回一个处理过后的函数;同时,我们可以在参数中传入一个数值timeout
,用来设置防抖函数的超时时间。
function debounce(fn, wait){
// ...
return function(){ ... }
}
将我们处理节流的部分逻辑拿过来;
function debounce(fn, timeout) {
let timer = null
return function(){
// 事件处理函数函数执行前,检测定时器是否存在
if (timer) {
// 如果定时器存在,则意味着函数在一定时间内被多次执行了,则清除定时器
clearTimeout(timer)
timer = null
}
// 然后设置新的定时器
timer = setTimeout(() => {
fn()
}, timeout)
}
}
这样,这个函数就可以返回一个被节流处理过的新函数。在timeout毫秒这段时间内,由于超时时间未达到,如果新函数被不断执行,真正的事件处理函数将不会执行,但新函数的执行会不断覆盖定时器,直到最后一个定时器超时后才执行真正的事件处理函数。
但目前封装的这个函数还是略有一些问题的:
- 传参问题。假设这里的
fn
是点击事件的处理函数,一般来说用到点击事件的时候,一般都会判断点中的是什么元素,因此fn
会接收一个MouseEvent对象,其中包含本次点击的相关信息(笔者喜欢把这个参数写为e
)。但被防抖处理后,这个函数就没法接受参数了。可以看到,上方的fn
是不带参数,直接被执行的。 this
指向问题。(其实笔者还没遇到这个问题,但网上找到的代码有这一处理,待笔者研究研究🤪)
因此,我们需要再进一步处理:
function debounce(fn, timeout) {
let timer = null
return function(){
// 保存参数与this指向
let args = arguments;
let ctx = this;
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
// 用apply执行代码
fn.apply(ctx,args)
}, timeout)
}
}
到此步骤,代码其实已经和网上搜索到的大部分防抖代码类似了。
好了,防抖就先告一段落吧,接下来说说它的好兄dei,传说中的“节流”。
节流
是什么?
这次把目光从鼠标点击事件转向元素的鼠标滚轮事件。滚轮事件有时我们用到的会非常多,例如地图的放大缩小。每当鼠标发生一次滚动,就会触发一次滚轮事件。然而当用户不断滚动的时候,可能会对页面性能产生一定影响。因此我们需要限制滚轮事件的触发频率,而且也应当让滚轮事件能够一直在执行(而不是使用防抖函数 —— 仅执行最后操作的那一次)。这时,节流就很有用了。
原理?
降低事件处理函数触发频率。
来试试?
<body style="margin:0;width:100vw;height:100vh">
<h1 style="margin:0" id="h1">滚一滚,有惊喜</h1>
<script>
function wheelHandler(e) {
console.log(e)
}
document.body.addEventListener('wheel', wheelHandler)
</script>
</body>
这里代码片段的用于为文档添加鼠标滚轮事件。当鼠标滚轮滚动时,触发wheel事件,并执行回调函数。
尝试运行一下:
笔者随便动了几下鼠标滚轮,就看到这一事件触发了多次。那如何限制一下呢?
先不管别的,试试上面写的防抖函数。
function wheelHandler(e) {
console.log(e)
}
document.body.addEventListener('wheel', debounce(wheelHandler,300))
果然如上文所说,在疯狂触发滚轮事件后,事件处理函数只执行了最后触发的那一次,并没有一直执行。(这说明我们上面写的防抖函数还算成功~)
那如何让事件处理函数在降低执行频率呢?
同样,我们在此对事件处理函数进行修改。
首先,简单地添加一个定时器。
let timer = null
function wheelHandler(e) {
timer = setTimeout(() => {
console.log(e)
}, 1000)
}
document.body.addEventListener('wheel', wheelHandler)
然而与上方防抖章节一样,此处仅仅是延迟了事件处理函数的执行时机,最终还是事件触发了多少次,事件处理函数执行多少次,并没有使事件处理函数执行频率降低。
接下来,需要继续对这个函数进行优化。
let timer = null
function wheelHandler(e) {
if (!timer) {
timer = setTimeout(() => {
console.log(e)
clearTimeout(timer)
timer = null
}, 1000)
}
}
document.body.addEventListener('wheel', wheelHandler)
此处的逻辑是,仅有定时器不存在(为null)的时候,事件处理函数才能够在一段时间超时后执行,且在一次执行过后,定时器将会被清除并置为null,使得的下一次触发执行的事件处理函数能够在时间超时后执行;若触发事件时发现定时器存在,则表示上次的事件处理函数还未执行,此次触发无效。
与业务分离
同样如函数的防抖,我们怎样实现一个类似如下的通用函数,来让其能够对任意函数进行节流?
(此处fn
表示要节流的函数-比如某些业务,wait
表示超时)
function throttle(fn, wait){
// ...
return function(){ ... }
}
嗯,首先还是把我们上方的逻辑拿过来,整理一下,得到:
function throttle(fn, wait) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn()
clearTimeout(timer)
timer = null
}, wait)
}
}
}
同样,此处函数也有无法接受参数以及this指向的问题。依葫芦(节流函数)画瓢即可。
function throttle(fn, wait) {
let timer = null
return function () {
let ctx = this;
let args = arguments;
if (!timer) {
timer = setTimeout(() => {
fn.apply(ctx,args)
clearTimeout(timer)
timer = null
}, wait)
}
}
}
总结一下
想不到今天距离上次写这篇文章都快过了1个月了,都快忘了;今天想起来就总结一下吧。
防抖
也就是将一个函数(函数a)的执行放到定时器中,当包裹的函数被不断执行时,判断定时器是否存在;如果存在,就清除旧定时器,建立新计时器,直到最后一个定时器超时的时候执行该函数(函数a)。因此防抖过后的函数(函数a)无论执行多少次,最终都仅执行最后一次。
节流
同样也是将函数(函数a)的执行放入到一个定时器中,当包裹的函数被不断执行时,判断定时器是否存在;如果不存在,就新建一个定时器,该定时器超时后直接执行函数(函数a);如果存在,则表示上次的函数还未执行,因此不进行任何操作。因此节流过后的函数(函数a)及时一段时间内被连续执行多次,也仅执行每个节流时间段结束后的那一次。
评论