此篇文章用于记录笔者学习防抖节流的理解,十分感谢 @起晚的蜗牛 的防抖(debounce) 和 节流(throttling),这篇文章帮助了笔者如何写出一个更高效的防抖,建议本篇的读者可以先浏览 @起晚的蜗牛 的这篇博客,其在他的文章中作了一些便于读者理解的图,本博客也将使用他的博客中的一部分图文。
注意,本篇文章未标原创,仅仅是笔者的一篇学习笔记,其中的内容是在上文提及的博客的基础上进行的理解梳理。
原文中的防抖高性能写法笔者也进行了思路分析,便于读者理解。
1. 防抖节流有什么用?
假设我们为当前页面添加了下面的监听函数,当页面滚动时,就会触发showTop
,这时候,我们如果仅仅是按一下方向键使页面向下滚动一点点,我们就能在控制台看到输出的一长串信息:
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = showTop
控制台:
而我们现在的showTop
仅仅是向控制台输出一条打印信息,试想,如果我们需要在页面滚动的时候向后台发送一个请求,那么,这时候就出现了请求过于频繁,然而实际上我们并不需要如此高频的反馈!这会牺牲我们前台和后端以及网络的性能。
而防抖节流就可以在一定程度上避免上文中的情况。
2. 防抖(debounce)
策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。 这是debounce的基本思想,在后期又扩展了前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。
延迟debounce,示意图:
前缘debounce, 示意图:
2.1 延迟防抖的简易实现
我们可以使用一个定时器来执行showTop
,在周期期间若showTop
再次被触发,那么清楚之前的定时器并重新创建一个定时器:
function debounce(fn,delay){
let timer = null
return () => {
if (!timer) { // 如果定时器不存在,就创建一个定时器来执行fn
timer = setTimeout(()=>{
fn() // 当执行完fn之后,需要将timer 设置为null
timer = null
},delay)
}else {
clearTimeout(timer)
timer = setTimeout(()=>{
fn()
timer = null
},delay)
}
}
}
function showTop () {
let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000)
这也是我一直实现防抖的方式,但是,在阅读了 @起晚的蜗牛 的博客之后,我才发现,通过这种方式来实现防抖,虽然也达到了降低触发频率的期望,但是,客户端需要频繁的创建和清除定时器,看似达到了我们的需求,实则是拆东墙补西墙,损耗了客户端的性能。
2.2 延迟防抖的高性能实现
为了客户端不需要频繁地创建清除定时器来实现防抖,我们可以使用时间戳。
周期内有新事件触发时,重置定时器开始时间撮,定时器执行时,判断开始时间撮,若开始时间撮被推后,重新设定延时定时器。
function debounce(fn,delay){
let timer = null
let triggerTime // 触发fn的时间
let run = (wait) => {
timer = setTimeout(()=>{
let executeTime = (new Date()).getTime() // 执行fn的时间
let alreadyWait = executeTime - triggerTime
if (alreadyWait < wait){
triggerTime = executeTime
run(delay - alreadyWait)
} else {
fn()
timer = null
}
}, wait)
}
return () => {
triggerTime = (new Date()).getTime() //重置fn的触发时间
if (!timer) {
run(delay)
}
}
}
function showTop () {
let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,3000)
这里的debounce
方法理解起来存在一定难度,读者可以结合下图进行理解。
- 当在时间1触发事件时,会执行:
triggerTime = (new Date()).getTime() //重置fn的触发时间
if (!timer) {
run(delay)
}
因为此时的timer=null
,所以执行run(delay)
2. 如果在时间1和时间2之间,我们没有再次触发事件,那么,我们将在时间2执行fn
(注意,我们忽略setTimeout的误差,实际情况中时间2-时间1>=delay间隔,这里我们姑且认为时间2-时间1=delay间隔,这里的误差不影响我们的思路),也就是alreadyWait=wait
,定时器执行else
分支中的流程:
if (alreadyWait < wait){
run(delay - alreadyWait)
} else {
fn()
timer = null
}
- 但是,如果我们在时间1到时间2之间再次触发了事件,这会更新
triggerTime
,在时间2时,我们将执行run(delay - alreadyWait)
,也就是再次构建一个定时器,等待时间为delay-alreadyWait
;一般的防抖是在事件的触发时间,即时间3,来重新创建一个定时器,等待时间为delay
。而现在,我们是在时间2的时候来创建定时器,所以等待时间由delay => delay - alreadyWait。
为什么要这样处理呢?
时间1到时间2之间无论我们触发了多少次事件,因为这段时间内的!timer
的值为false,所以这段时间内触发的事件响应只是更新了triggerTime,而当我们在时间2执行定时器的内容时,只会获取到最新的triggerTime(也就是时间1到时间2期间最后一次事件触发时间),由于triggerTime的改变,定时器会执行run(delay - alreadyWait)
,这时候才会创建一个新的定时器。所以说,无论我们在时间1到时间2之间触发了多少次该事件,定时器也最多也只会创建两次,性能得到了优化。
2.3 前缘防抖的高性能实现
前缘防抖和延迟防抖的实现类似:
immediate表示是否使用前缘防抖
function debounce(fn,delay,immediate=false){
let timer = null
let triggerTime // 触发fn的时间
let run = (wait) => {
console.log('create timer')
timer = setTimeout(()=>{
let executeTime = (new Date()).getTime() // 执行fn的时间
let alreadyWait = executeTime - triggerTime
if (alreadyWait < wait){
run(delay - alreadyWait)
} else {
if (!immediate){
fn()
}
timer = null
}
}, wait)
}
return () => {
triggerTime = (new Date()).getTime()
if (!timer) {
if (immediate){
fn()
}
run(delay)
}
}
}
3. 节流(throttling)
throttling,节流的策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期。
延迟throttling示意图:
前缘throttling 示意图:
immediate表示是否采用前缘节流
function throttling(fn,delay,immediate=false){
let timer = null
return () => {
if (!timer){
if (immediate){
fn()
}
timer = setTimeout(()=>{
if (!immediate) fn()
timer = null
},delay)
}
}
}