最近的面试中考到了debounce
,函数防抖,笔试的时候答的不是特别好,下来好好研究了一下,从原理到优化,再到开源工具库lodash
的实现源码,梳理了一番,现整理以下。面试
先简单介绍一下debounce
,从最简单的一个场景入手,当用户不断点击页面,短期内频繁的触法点击事件,只有在用户触法事件后的n
s时间内,没有再触法事件,真正的监听函数才会执行,若是在这段时间内再次触法了事件,就须要从新计算这个n
s。app
debounce
最主要的做用是把多个触法事件的操做延迟到最后一次触法执行,在性能上作了必定的优化。dom
debounce
若是不使用debounce
,那就会每一次点击都会触法事件的回调函数,这有时候对于性能是一种巨大的浪费(好比大量的增长dom
元素)。或者当回调函数计算量很大的时候,甚至会致使阻塞。函数
window.addEventListener('click', function (event) { var p = document.createElement('p') p.innerHTML = 'trigger' document.body.appendChild(p) })
频繁触法
能够看出,每一次点击都会触法函数执行。工具
debounce
window.addEventListener('click', debounce(function (event) { var p = document.createElement('p') p.innerHTML = 'trigger' document.body.appendChild(p) return 'aaaa' }, 500))
debounce优化
能够看出,只有在最后一次点击的500ms
后,真正的函数func
才会触法。性能
debounce
本篇文章的debounce
实现主要参考了lodash
库,会从最基础的实现开始,一步步完善它。debounce
的核心实现,就是要判断每次触法事件的时候,要不要执行真正的func
。优化
大致思路就是每次触法事件都开启一个延时的定时器,在定时器结束的时候对比与最后一次触法事件时的时间差,若是时间差大于延迟的阈值,那么就执行真正的func`。this
大体的结构以下spa
function debounce (func, wait) { var lastCallTime // 最后一次触法事件的时间 var lastThis // 做用域 var lastArgs // 参数 var timerId // 定时器对象 wait = +wait || 0 // 启动定时器 function startTimer (timerExpired, wait) { return setTimeout(timerExpired, wait) } // func函数执行 function invokeFunc () { } // 调用func函数的断定条件 function shouldInvoke () { } // 定时器的回调函数 function timerExpired () { // 在这里判断触法事件的时间差 } // 要返回的函数 function debounced (...args) { } return debounced }
这就是基本的debounce
函数的构成,下面边解析,边去一一填充这些函数,最后再对函数进行一步步的优化。code
debounced
每一次触法事件的时候都会进入到这个函数,这个函数须要作这么几个事情。
lastCallTime
timerId
function debounced (...args) { const time = Date.now() lastThis = this lastArgs = args lastCallTime = time timerId = startTimer(timerExpired, wait) }
startTimer
startTimer
就是启动一个定时器,后续会有更多的拓展,因此封装一个函数
function startTimer (timerExpired, wait) { return setTimeout(timerExpired, wait) }
timerExpired
timerExpired
主要判断是否执行func
function timerExpired () { const time = Date.now() if (shouldInvoke(time)) { return invokeFunc() } }
shouldInvoke
shouldInvoke
判断每次事件触法的时间差,若是大于阈值,那么真正的func
就会执行
function shouldInvoke (time) { return lastCallTime !== undefined && (time - lastCallTime >= wait) }
invokeFunc
function invokeFunc () { timerId = undefined const args = lastArgs const thisArg = lastThis let result = func.apply(thisArg, args) lastArgs = lastThis = undefined return result }
这样,这个函数就写完了。把每一步拆解开来,理解仍是相对容易的,再总结一下。每一次触法事件,都开启一个定时器timerId
,而且会更新触法事件的最后时间lastCallTime
,在定时器的回调函数里面,判断回调函数的执行时间与lastCallTime
的时间差,若是大于阈值,说明延迟时间到了,func
执行,若是小于,就忽略。
虽然实现了基本的debounce
,但在扩展它的功能以前,看一看有没有优化的空间,每一次触法事件都开启一个定时器是否是太浪费了。这里可不能够减小调用次数。
把开启定时器的逻辑放在timerExpired
能够大大减小定时器的数量。debounced
开启了第一次定时器后,debounced
会忽略后面的定时器开启,直到func
执行以后(timerId
为undefined
),而在timerExpired
里面判断若是func
不知足触发条件,那么就开启下一个定时器。
其实本质就是确保上一个定时器的回调不会触法func
了,才会开启下一个定时器。
优化代码以下
function timerExpired () { const time = Date.now() if (shouldInvoke(time)) { return invokeFunc() } timerId = startTimer(timerExpired, wait) }
function debounced (...args) { const time = Date.now() lastThis = this lastArgs = args lastCallTime = time if (timerId === undefined) { timerId = startTimer(timerExpired, wait) } }
timerExpired
中开启的定时器
timerId = startTimer(timerExpired, wait)
延迟的时间是否必定为wait
呢,这是不必定的。
举个例子,好比wait
为5
,此时在某一个定时器的回调函数timerExpired
检测到上一次触法事件的lastCallTime
为100
,而Date.now()
为103
,此时虽然103-100 = 3 < 5
,要开启下一次定时,但这个时候定时的时间为 5 - 3 = 2
就能够了。这才是精确的时间。
因此咱们须要把这个时间封装成一个函数remainingWait
function remainingWait(time) { const timeSinceLastCall = time - lastCallTime const timeWaiting = wait - timeSinceLastCall return timeWaiting }
function timerExpired () { const time = Date.now() if (shouldInvoke(time)) { return invokeFunc() } timerId = startTimer(timerExpired, remainingWait(time)) }
附上执行的流程图
这其实只是实现了一个basicDebounce
,其实有的时候咱们须要在频繁触法事件的开始当即执行func
,而忽略后面的触法事件,这就须要加入参数控制,也就是lodash
中的trailing
和leading
,甚至二者同时存在,头尾各执行一次,还有就是throttle
函数节流,保证在一段时间内func
至少执行一次,这就是lodash
中的maxWait
参数。下一篇文章会完善这些功能,届时,一个完整的debounce
才是真正的实现了。