lodash源码中debounce函数分析
lodash源码中debounce函数分析
⼀、使⽤
在lodash中我们可以使⽤debounce函数来进⾏防抖和截流,之前我并未仔细注意过,但是不可思议的是,lodash中的防抖节流函数是⼀个函数两⽤的,所以今天我就简单分析⼀下,它是如何做到的;
// 情况⼀:当resize事件触发后,并不会⽴即执⾏,⽽是会直到密集程度⼤于150的时候,才会触发calculateLayout执⾏;也就是我们常说的防抖函数;jQuery(window).on('resize', _.debounce(calculateLayout,150));
// 情况⼆:当点击时 `sendMail` 随后就被调⽤。此后的300毫秒内触发的将会被忽略,300毫秒之后的触发才会⽣效,⼜会计算⼀个新的周期;这就是我们常说的节流函数;
jQuery(element).on('click', _.debounce(sendMail,300,{
'leading':true,
'trailing':false
}));
通过上⾯的分析我们会发现,在同⼀个函数中,实现了防抖和节流的效果;
⼆、源码分析
先来认识⼀下⼀部分的准备代码;
const freeGlobal =typeof global ==='object'&& global !==null&& global.Object === Object && global;
// 这是⽤来判断当前环境的;实际上在浏览器环境下;global指向window;
函数中,如果未定义wait的情况下会优先使⽤requestAnimationFrame动画api; 否则就使⽤setTimeout;
const useRAF =(!wait && wait !==0&&questAnimationFrame ==='function');
整个debounce函数形如下⾯的结构;闭包了⼀部分的全局变量;然后返回⼀个函数,函数执⾏逻辑根据闭包的全局变量为依据;实际上我们写的很多⾼阶函数都需要借助这样的思想;
function debounce(a , b){
let c,d,e;
return function(){
let res =use(c,d,e);
//
}
}
在lodash中节流的思路是这样⼦的;先提供两个闭包的变量leading , trailing ;leading控制是否是节流函数; trailing控制是否是防抖函数;这⾥我们先讨论节流的思路;返回的函数肯定是⼀直会被触发的,因此我们需要做到的是,第⼀次触发时,直接执⾏,此后触发的直接忽略,直到超过预定等待时间后,触发的才执⾏,实际上相当于将密集的触发,稀疏成为⼀定的周期触发⼀次,这就是所谓的节流;
那我们就需要先办法实现⼀个函数;能够判断当前的函数是否应该实⾏;这个函数可以这样写;
function shouldInvoke(time){
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return(lastCallTime ===undefined||(timeSinceLastCall >= wait))
}
它表达的意思可以是每次将当前时间与上⼀次触发的时间做⽐较,如果差值⼤于预定周期,那么就说明可以执⾏;要么是第⼀次执⾏,上⼀次触发的时间是undefined,这个函数也会执⾏;
整体的代码可以像下⾯这样;
function debounce(func, wait, options){
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime;
let lastInvokeTime =0
let leading =false
let maxing =false
let trailing =true
if(typeof func !=='function'){
throw new TypeError('Expected a function')
}
wait =+wait ||0
if(isObject(options)){
leading =!!options.leading
trailing ='trailing'in options ?!!ailing : trailing
}
function invokeFunc(time){// invoke : 调⽤的意思;
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis =undefined
lastInvokeTime = time // 每⼀次这个函数的执⾏时间,就是更新全局lastInvokeTime的时间⾄函数执⾏的时刻; result =func.apply(thisArg, args)// func的执⾏内部this指向全局的this , 参数列表指向lastArgs;
return result
}
function leadingEdge(time){
lastInvokeTime = time
return leading ?invokeFunc(time): result
}
function shouldInvoke(time){
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
return(lastCallTime ===undefined||(timeSinceLastCall >= wait))
}
function debounced(...args){
const time = w()
const isInvoking =shouldInvoke(time)// true
lastArgs = args
lastThis =this
lastCallTime = time
if(isInvoking){
if(timerId ===undefined){
return leadingEdge(lastCallTime)
}
}
return result
}
return debounced
}
所以节流的思想其实很简单,就是对于每⼀次单位时间的触发,如果符合条件,调⽤,不符合条件,忽略,是否符合条件的关键在于当前调⽤时间与第⼀次触发的时间的时间差是否⼤于预定周期;
三、防抖函数
lodash中的防抖函数写的就⽐较⿇烦了,⼀般我们⾃⼰实现的思路,可以是这个样⼦;
先定义⼀个wait时间内执⾏的定时器,然后如果触发频率⼤于了wait就直接执⾏这个定时器所注册的函数;如果没有,就清除当前定义的定时器;从新从触发时间开始计时的期限为wait周期的定时器;等价于不断推进⼀个wait周期的定时器;这种思路是可以的;但是lodash实现的⽅式并⾮这样;它是第⼀次触发的时候开始了⼀个wait周期的定时器;然后每次触发,并不清除定时器,⽽是保存着触发的时间;然后在wait周期后判断是否执⾏注册的函数;判断的标准就是看当前时间和保存的时间差是否⼤于wait,如果⼤于或者等于就直接执⾏函数,否则就看⼀下,差值是多少,再次定义⼀个差值的定时器,再次检验,通过这样的⽅式,就可以最终检验到是否可以执⾏函数;达到防抖的⽬的;它的核⼼有下⾯⼏段;
function leadingEdge(time){
/
/ Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId =startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ?invokeFunc(time): result
}
function timerExpired(){
const time = w()
if(shouldInvoke(time)){
return trailingEdge(time)
}
// Restart the timer.
timerId =startTimer(timerExpired,remainingWait(time))
}
function startTimer(pendingFunc, wait){
if(useRAF){
root.cancelAnimationFrame(timerId)
questAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function remainingWait(time){
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall // 主要是这⼀个
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
四、完整代码
function debounce(func, wait, options){
let lastArgs,
lastThis,
maxWait,
result,
timerId,
pendinglastCallTime;
let lastInvokeTime =0
let leading =false
let maxing =false
let trailing =true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF =(!wait && wait !==0&&questAnimationFrame ==='function')// 看⼀下当前环境是否可以使⽤requestAnimationFrame
if(typeof func !=='function'){
throw new TypeError('Expected a function')
}
wait =+wait ||0
if(isObject(options)){
leading =!!options.leading
maxing ='maxWait'in options
maxWait = maxing ? Math.max(+options.maxWait ||0, wait): maxWait // 配置项中的maxWait和wait更⼤的那⼀个;
trailing ='trailing'in options ?!!ailing : trailing
}
function invokeFunc(time){// invoke : 调⽤的意思;
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis =undefined
lastInvokeTime = time // 每⼀次这个函数的执⾏时间,就是更新全局lastInvokeTime的时间⾄函数执⾏的时刻;
result =func.apply(thisArg, args)// func的执⾏内部this指向全局的this , 参数列表指向lastArgs;
return result
}
// 开始⼀个任务;
function startTimer(pendingFunc, wait){
if(useRAF){
root.cancelAnimationFrame(timerId)
questAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 结束⼀个任务;
function cancelTimer(id){
if(useRAF){
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time){
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId =startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ?invokeFunc(time): result
}
function remainingWait(time){
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time){
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论