-
前言
防抖(debounce) 和 节流(throttle) 是前端性能优化的常见策略,也是面试中的高频考题。遗憾的是,很多新手小伙伴往往对这两个概念只是一知半解,不知道这两种策略有什么区别。 故此,本文希望引入一个小例子来帮助各位了解防抖和节流。
-
防抖和节流是什么?
在我们的开发过程中,常常需要监听 mousemove , scroll, resize 等事件,而这些事件都是高频触发的,倘若我们的回调逻辑中有比较耗时的操作的话,就会造成页面抖动、卡顿等现象。
我们来看看这个小例子,我们有一个小方块,然后监听了它的 mousemove 事件,该事件每触发一次就改变小方块中的文字,显示mousemove触发的次数,简单代码如下:
<div class="box">0</div>
.box { width: 200px; height: 200px; margin: 100px auto; background-color: yellow; color:#333; display: flex; align-items: center; justify-content: center; cursor: pointer; }
const box = document.querySelector(".box"); let count = 0; let update = () => box.innerHTML = ++count; // 每次触发就更改将count显示到box中并递增 box.addEventListener('mousemove',update);
代码很简单,这里就不多加赘述了,接下来看看执行结果
可以看到,随着鼠标的移动,box中的文字不断地被刷新,也就意味着我们添加的回调函数不断地在被调用,这里回调逻辑很简单,因此看不出有什么影响,倘若回调函数是一个非常耗时的操作的话,就很大概率会造成卡顿现象。 防抖和节流都是用降低事件回调触发的频率来实现性能优化,那么具体是怎么实现的呢?我们一起来看一下。
-
防抖的原理和实现
在原来的逻辑中,我们的回调是同步执行的,也就是说每当事件触发就执行一次回调。防抖将原来的同步执行改为异步执行,即等待一段时间后执行,当第一次事件触发时,通知浏览器等n毫秒后执行这个回调,如果在等候的n毫秒内没有新的事件触发就执行该回调,如果有新的事件触发则通知浏览器取消掉之前的回调,并重新计时,等待n毫秒后再执行新的回调,如此周而复始。 下面结合代码来看下:
// fn 待处理的函数 // delay 延迟执行的时间 let debounce = (fn,delay) => { // 每次生成的异步任务的标识 let timer = null; return function (...args) { // 清除上次的调用 clearTimeout(timer); let that = this; // 等待 delay 时间过后再执行 timer = setTimeout(() => { // 通过apply和that保证this的正确指向 fn.apply(that,args); }, delay); } }
在代码中debounce
返回了一个闭包,闭包中的clearTimeout
用来取消之前的回调,然后重新计时,在delay时间之后再调用回调。注意以下几个关键点:
- 函数返回一个闭包
- fn 回调被setTimeout延迟调用
- clearTimeout 会清除之前的调用,保证delay秒后执行的始终是最后一次调用
- 每次执行都会调用setTimeout重新计时,所以有可能很久才执行一次回调
- 需要用apply调用来保证this的正确指向,代码中为了方便理解用了that保存this,但由于箭头函数的特性,直接写this也是可以的
看看效果:
box.addEventListener('mousemove',debounce(update,500));
可以明显地看到,回调的执行频率被大大地降低了,优化完成。
-
节流的原理和实现
在讲节流之前,我们先看看下图:
这是我们生活中很常见的节流阀,不管左边管道的水是多么滔滔不绝,通过节流阀的控制,从右边出来的都只能是涓涓细流,这就是节流的思路。
节流也是将原来的同步调用转为了异步调用,等待一段时间后再执行。同防抖一样,在第一次事件触发的时候, timer
为null
,setTimeout
通知浏览器等待n毫秒后执行回调,并用timer
保存setTimeout
的返回值,作为此次任务的标识。 如果在等待执行的n毫秒内又有新的事件触发,由于此时timer
不等于null
,直接返回。在n毫秒回调函数执行之后,将timer
重置为null
,保证下次事件触发能正常执行回调,如此周而复始,下面看下代码实现:
// fn 待处理的函数 // delay 延迟执行的时间 let throttle = (fn,delay) => { let timer = null; return function (...args) { // 如果timer有值,代表之前调用过了 if (!timer) { let that = this; // 转化为异步调用,delay 时间过后再执行 timer = setTimeout(() => { // 清空timer,让下一次调用能成功生效 timer = null; // 通过apply和that保证this的正确指向 fn.apply(that,args); }, delay); } } }
需要注意以下几个关键点:
- 函数返回一个闭包
- fn 回调被setTimeout延迟调用
- timer不为空,代表在delay时间内,已经有了一次调用,直接返回
- 在delay时间后,需要重置timer并且执行回调,保证下一次调用能成功生效
- 需要用apply调用来保证this的正确指向,代码中为了方便理解用了that保存this,但由于箭头函数的特性,直接写this也是可以的
看看效果:
box.addEventListener('mousemove',throttle(update,500));
可以明显地看到,在500ms内,我们的节流函数只会执行一次,优化完成。
-
总结
防抖和节流的实现大致相同,都是将原来的同步调用改为异步调用,不同点在于防抖很严格,只要有新的事件刷新,就会抛弃之前的异步执行任务,重新生成新的任务,而节流相比防抖要稍微温和一点, 在delay时间内的多次事件触发,都能保证在delay时间后能执行一次回调。
本文对防抖和节流的实现只是简单实现,实际情况下还有更多的优化空间,例如参数检测,防抖和节流立即执行版本,节流的时间戳版本,这些以后有空了会做一个总结。
这是我自学前端过程中的第一篇博客,更多地是对自己所学知识的总结,如有纰漏,还请见谅!
本文使用 mdnice 排版