概述
一些事件被连续触发引起的频繁执行JavaScript代码会导致用户体验较差,如果处理事件的逻辑较为复杂甚至可能会引起页面假死的现象,使用事件防抖(debounce)和事件节流(throttle)可以解决这个问题。
问题的引出
考虑下面的情形,当某个视图处理逻辑是配合浏览器窗口的大小变动而响应执行时,会出现怎样的问题。
1 | window.onresize = e => { |
如果事件的处理逻辑相应比较简单,浏览器执行起来较快,好像问题也不大,然而有的时候由于窗口大小被改变,页面中的很多元素大小尺寸也需要跟着改变,这是如果每一个resize事件都要处理,就会消耗较多资源,并且一定程度引起页面卡顿,用于体验不好。
怎样解决这个问题呢?不难发现,window.onresize连续触发之后总有一个结束点,如果能在最后一次响应时执行真正需要的处理逻辑的函数,这样就能避免大量的重复调用。关键的问题是,如何确定连续的函数调用的最后一次调用是什么时候。
由事件频繁触发引起的函数连续调用,无论如何都会有一个时间间隔,为了确定最后一次调用的时间,可以假定一个时间间隔(比如100ms),如果超过这个时间函数没有被调用,则可以判断当前的连续调用已经结束,可以真正开始执行处理函数了。
函数防抖
JavaScript中由于函数调用太过频繁而预先设置一个时间间隔,如果超过这个间隔函数没有被调用,才真正开始执行,否则不执行继续等待之后的调用的设计,被称为函数防抖(debounce)。
一个典型的例子就是impress.js,它是一个在浏览器的展示幻灯片的工具,当打开impress.js的官方案例时,如果调整浏览器窗口的大小会发现,幻灯片的大小只有在窗口真正稳定下来(用户不再调整窗口的大小之后)才会重新适应新的窗口的大小。
函数防抖的简单实现
为了让目标函数在间隔一定时间之后执行,需要用到的setTimeout函数,但是单一的setTimeout函数只会推迟函数的调用,因此需要一个简单的设置,每次都把之前设置的定时器清除掉。
1 | const debounce = (func, wait = 100) => { |
debounce函数接收2个参数,第1个是需要防抖的目标函数,第2个是防抖等待的时间wait(默认值100ms)。debounce是一个闭包,它返回的函数有一个私有变量timeout,当函数每次触发时,timeout都被清除一次,然后重新设置一个新的定时器,用于执行防抖目标函数,定时器等待的时间是wait毫秒。
接着测试window.onresize事件,尝试改变浏览器窗口的大小,会发现无论改变窗口的速度多快,只要500ms内又重新改变了窗口,之前触发的逻辑都不会被执行。
1 | window.onresize = debounce(e => console.log('hello world'), 500); |
除此之外,还可以测试目标函数执行的上下文(this和函数参数),这里可以尝试在目标函数中输出this和e试试,会发现和正常情况下(不适用debounce绑定目标函数)一样,this和函数参数都能被正常使用。
这是因为debounce中返回的是一般的函数声明而没有用箭头函数,而里面的setTimeout却是箭头函数,这样就保证了防抖目标函数被调用时的上下文和正常情况下一样。
1 |
|
函数节流
除了函数防抖,还有一个相关的概念叫函数节流(throttle)。
函数节流就是当目标函数被连续频繁调用时,目标函数并不会每次调用都被执行,而是间隔一个设定好的时间再执行。
鼠标滑过某个区域的时候,如果处理逻辑的计算量或者消耗资源较大,这时使用函数节流可以有效提高用户体验。
函数节流的简单实现
1 | const throttle = (func, wait = 50) => { |
这里创建一个函数节流的简单实现,每次调用都记录一下当前时间,看看是否和上次调用的时间差是否大于指定的时间差,如果大于则调用目标函数,并且记录当前时间为新的上一次调用时间。
同样地,这里也处理好了目标函数的调用上下文(this和参数)。
1 |
|
实现上面的代码,试着在页面上滑动,检查控制台,会发现都是间隔200毫秒左右才输出一次的。
一般来说,间隔的时间应该根据业务的不同也不同,如果时间太大,会使效果间断时间太长,降低用户体验。
总结
简单介绍了一下函数防抖和函数节流的基本,感兴趣的同学可以去看一下lodash的相关实现,lodash的底层有更多优化而且支持更多配置。