debounce也就是防抖函数,什么是防抖,防抖主要就是用在用户在一定事件内多次触发事件的时候,只执行一次,其实现原理主要是用定时器的机制实现,下面看看lodash中的debounce函数
lodash中的debounce函数接受三个参数:func, wait, options 1、func就是需要做防抖处理的函数 2、wait就是延迟多长时间触发 3、options配置项,可有maxwait-最大等待时长等参数 最终返回的是一个 debounced函数,debounce 具体代码:
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
说一下其中出现的函数: 1、shouldInvoke函数,这个函数源码是这样的
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the 'maxWait' limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
直接看返回结果,
1、lastCallTime === undefined表示是第一次触发,因为lastCallTime默认值就是undefined; 在debounce函数的开头,定义了这一些变量
2、timeSinceLastCall >= wait 表示两次触发的间隔已经超过了wait,也就是可以再次触发;
3、lastInvokeTime 在 invokeFunc 中被赋值为lastCallTime(后面解析invokeFunc函数),所以timeSinceLastCall < 0 几乎不可能出现,在第一次触发,然后调整过计算机时间时可能会出现 timeSinceLastCall < 0 的情况;
4、maxing为真表示使用防抖函数时,传入了options参数,切options参数中有maxwait属性;timeSinceLastInvoke >= maxWait,lastInvokeTime初始值为undefined,后被赋值为lastCallTime,所以timeSinceLastInvoke >= maxWait也表示两次触发的时间间隔超过了最大等待时长
所以在初次进入到debounced函数中,会进入到if(isInvoking)条件判断内(后面触发防抖的话,就不会进入条件判断了),timerId在第一次触发时,因为定义时未赋值,所以为undefined,代码继续往下执行,调用了leadingEdge方法,并传入lastCallTime参数,现在看下leadingEdge方法:
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
// 如果传入了optios并且options是个对象,leading就是 !!options.leading,否则,默认为false
// 在invokeFunc中对result赋值过
return leading ? invokeFunc(time) : result
}
很简单的逻辑,就是将lastInvokeTime赋值为lastCallTime(也就是time,为Date.now()),然后timerId赋值为startTimer(timerExpired, wait),看下startTimer函数内部:
function startTimer(pendingFunc, wait) {
if (useRAF) {
// cancelAnimationFrame用于取消 requestAnimationFrame方法调用计划的动画帧请求
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
在debounce函数的开头,有定义useRef,
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
简单解释就是调用decounce函数时,如果未传入wait,或则传入的wait为0,root.requestAnimationFrame是一个函数(root为Window对象,所以root.requestAnimationFrame === 'function'即表示在浏览器环境下),此时useRef就为true;
requestAnimationFrame和cancelAnimationFrame是浏览器的两个api,
额外小知识:
requestAnimationFrame官方解释:告诉浏览器,我需要执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画,这么做的目的就是,如果wait为0,那么定时器就不会延迟执行(这里不讨论宏任务和微任务的问题),也就是setTimeout(callfunc,0),我们都知道帧动画最低每秒60帧,再低就会看到卡顿的现象,也就是掉帧;浏览器也遵循这个规则,所以一帧大概是16ms左右,也就是浏览器每16ms重绘一次(现在浏览器可能会根据屏幕刷新率做出调整),所以定时器就算是延迟0s执行,浏览器也不会立即做出响应,还是要等重绘时才能看到dom的更新,而且定时器属于宏任务,会被浏览器添加进宏任务队列,遵循事件循环机制,所以一般定时器的延迟要高于其设定的延迟时间,所以requestAnimationFrame要优于定时器的,而cancelAnimationFrame也就是取消requestAnimationFrame提交的回调
回归正题,timerId被赋值了,在看下调用startTimer函数时传入的timerExpired函数,其代码如下:
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
前面已经解读过shouldInvoke函数了,其值如果为true,返回一个新函数trailingEdge(time),否则就重置timerId的值,其中还涉及另外一个新的函数--remainingWait,先看下trailingEdge函数,代码如下:
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
直接看其注释的意思,只有在有lastArgs时才调用,这意味着func至少已经防抖过一次了。也就是func并不是第一次调用了,总结就是,未防抖过,就返回invokeFunc(time)的值,否则返回result。
继续解读invokeFunc函数,其代码如下:
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
其实也就是将使用apply改变func调用时的this指向,并将结果赋值给result
再看remainingWait函数,其代码如下:
function remainingWait(time) {
// 在debounce中对 lastCallTime 赋值为当前时间
const timeSinceLastCall = time - lastCallTime
// lastInvokeTime 默认值为0,中途有赋值为time,看函数是否执行
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
直接看return后面的代码,前面已经说过maxing的来历,默认为false,赋值为maxing = 'maxWait' in options
,timeWaiting = wait - timeSinceLastCall;timeSinceLastCall = time - lastCallTime;time为执行到timerExpired函数时从新取的Date.now(),而lastCallTime是在执行debounced函数时取的Date.now()(每次触发函数都会重新赋值的),所以它们两个之间是有一个时间差的,timeSinceLastCall就姑且理解为代码执行到现在所用的时间吧。
timeSinceLastInvoke = time - lastInvokeTime,lastInvokeTime是在invokeFunc函数中为其赋值的,而invokeFunc函数执行的时候,也就是setTimeout执行的时候或则是requestAnimationFrame回调执行的时候,但是time是在debounced执行之初传入的,所以lastInvokeTime的值为第一次调用debounced函数时的时间戳,后续触发防抖的时候,并不会对lastInvokeTime重新赋值,直到func第一次执行完,也就是过完wait时间之后再次触发才会被重新赋值;所以timeSinceLastInvoke的值表示初次触发函数到现在的时间差
回到leadingEdge函数中,其返回值为 return leading ? invokeFunc(time) : result
,leading默认值为false,后被赋值为 !!options.leading。
这里也就是要不要执行invokeFunc函数的问题,如果options中没有传leading参数,就不执行,result为undefined,否则执行invokeFunc函数,result被赋值为func.apply(...)
至此,调用debounce时不传入options的情况已经解读完毕,所涉及到的函数也已经解读完,至于debounced函数中后面的条件判断,请看下面的流程图: 其中最主要的就是在startTimer中,传入的timerExpired,因为是直接将timerExpired方法添加进定时器的,而在timerExpired方法中满足条件时,返回的是trailingEdge函数的执行结果,在trailingEdge函数中,满足条件又返回的是invokeFunc函数的执行结果,在invokeFunc函数中,就执行了func.apply(thisArg args),至此,执行了传入的函数(有种俄罗斯套娃的感觉),达到了防抖的目的
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!