You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functiondebounce(func,wait){lettimerIdif(typeoffunc!=='function'){thrownewTypeError('Expected a function')}wait=+wait||0functiondebounced(){clearTimeout(timerId)timerId=setTimeout(func,wait)}}
functiondebounce(func,wait){lettimerIdconstuseRAF=(!wait&&wait!==0&&typeofroot.requestAnimationFrame==='function')if(typeoffunc!=='function'){thrownewTypeError('Expected a function')}wait=+wait||0functiondebounced(){if(useRAF){returnroot.cancelAnimationFrame(timerId)}else{clearTimeout(timerId)}if(useRAF){root.cancelAnimationFrame(timerId)timerId=root.requestAnimationFrame(func)}else{timerId=setTimeout(func,wait)}}}
现在代码有点复杂了,可以将开启计数器,和清除计数器的逻辑抽象成两个函数:
functiondebounce(func,wait){lettimerIdconstuseRAF=(!wait&&wait!==0&&typeofroot.requestAnimationFrame==='function')if(typeoffunc!=='function'){thrownewTypeError('Expected a function')}wait=+wait||0functionstartTimer(pendingFunc,wait){if(useRAF){root.cancelAnimationFrame(timerId)returnroot.requestAnimationFrame(pendingFunc)}returnsetTimeout(pendingFunc,wait)}functionclearTimer(id){if(useRAF){returnroot.cancelAnimationFrame(id)}clearTimeout(id)}functiondebounced(){clearTimer(timerId)startTimer(func,wait)}}
参数、this 及返回值
目前的实现,在最后调用 func 的时候是没有传入参数的,而且函数调用的时候的 this 也没有绑定,可能跟预期的指向不一致,也没有返回值。
functiondebounce(func,wait){letlastArgs,lastThis,result,timerIdconstuseRAF=(!wait&&wait!==0&&typeofroot.requestAnimationFrame==='function')if(typeoffunc!=='function'){thrownewTypeError('Expected a function')}wait=+wait||0functioninvokeFunc(){constargs=lastArgsconstthisArg=lastThislastArgs=lastThis=undefinedresult=func.apply(thisArg,args)returnresult}functionstartTimer(pendingFunc,wait){if(useRAF){root.cancelAnimationFrame(timerId)returnroot.requestAnimationFrame(pendingFunc)}returnsetTimeout(pendingFunc,wait)}functionclearTimer(id){if(useRAF){returnroot.cancelAnimationFrame(id)}clearTimeout(id)}functiondebounced(...args){lastArgs=argslastThis=thisclearTimer(timerId)startTimer(invokeFunc,wait)}}
functiontimerExpired(){consttime=Date.now()if(shouldInvoke(time)){timerId=undefinedreturninvokeFunc(time)}// Restart the timer.timerId=startTimer(timerExpired,remainingWait(time))}
functiondebounced(...args){consttime=Date.now()constisInvoking=shouldInvoke(time)lastArgs=argslastThis=thislastCallTime=timeif(isInvoking){if(timerId===undefined){returnleadingEdge(lastCallTime)}if(maxing){// Handle invocations in a tight loop.timerId=startTimer(timerExpired,wait)returninvokeFunc(lastCallTime)}}returnresult}
functiondebounced(...args){consttime=Date.now()constisInvoking=shouldInvoke(time)lastArgs=argslastThis=thislastCallTime=timeif(isInvoking){if(timerId===undefined){returnleadingEdge(lastCallTime)}if(maxing){// Handle invocations in a tight loop.timerId=startTimer(timerExpired,wait)returninvokeFunc(lastCallTime)}}if(timerId===undefined){timerId=startTimer(timerExpired,wait)}returnresult}
本文为读 lodash 源码的第一百七十三篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash
gitbook也会同步仓库的更新,gitbook地址:pocket-lodash
依赖
《lodash源码分析之isObject》
《lodash源码分析之root》
源码分析
debounce
就是我们通常所说的防抖,防抖的作用是防止函数在一段时间内过快而又无效的执行。防抖的原理是在事件触发时,会记录当前事件触发的时间,如果在允许等待的时间
wait
内又触发了事件,则函数还不会执行,但是以这个时间为准,再等wait
的时间,如果再无事件触发,函数才会执行。debounce
最经典的应用场景是用在搜索建议上,搜索建议会在你输入结束或者输入时间间隔比较长时,才会请求获取建议列表。最简单实现
知道原理后,可以很简单地实现一个
debounce
函数:这样就实现了最简单的
debounce
函数了。但是用户在使用的时候,可能会不按照要求传入参数,为了更加严谨和友好,再对参数进行一些处理。
我们希望传入的
func
为一个函数,如果不是函数则报错;希望wait
是一个数字,如果不是数字,则转换成数字类型。代码修改如下:
wait
转换成数字后,也有可能是NaN
的情况,这里默认为0
。使用
requestAnimationFrame
优化如果
wait
没有传递,按照之前的实现,是使用setTimeout
,时间设置为0
来实现的。现代浏览器有一个
requestAnimationFrame
的api
,会在浏览器重绘之前调用,性能比setTimeout
更好。因此可以这样判断是否需要使用
requestAnimationFrame
:只有在
wait
为假值, 并且wait
也没有指定为0
,并且当前环境支持requestAnimationFrame
的情况下,才使用requestAnimationFrame
。代码更改如下:
现在代码有点复杂了,可以将开启计数器,和清除计数器的逻辑抽象成两个函数:
参数、
this
及返回值目前的实现,在最后调用
func
的时候是没有传入参数的,而且函数调用的时候的this
也没有绑定,可能跟预期的指向不一致,也没有返回值。因为最后返回调用的函数是
debounced
,因此可以将debounced
修改如下:因为
func
是延后调用的,lodash
在对返回值的处理是返回上一次调用func
的结果。同样,我们也可以对这部分逻辑进行抽离。
提取出来的
invokeFunc
会对lastArgs
和lastThis
做一些重置的工作。最大等待时间
现在
debounced
的设计是,如果事件触发的间隔总不超过wait
,则会一直不会调用到func
,但是有些场景下,我们希望可以设置一个最大的等待时间maxWait
,如果上一次调用func
的时间间隔超过maxWait
,则无论事件的触发间隔是否超过wait
,都会调用func
。考虑到这个是可选的配置,我们为
debounce
增加第三个参数options
,options
为对象,方便后续的扩展。增加
maxWait
后,我们需要对maxWait
进行取值:用
maxing
来表示有没有传入maxWait
参数,也即表示要不要开启最大等待时间这个功能。用户还可能传入
maxWait
的值比wait
还要小,因此要取maxWait
和wait
之间的较大值来作为最大等待时间。因为涉及到了两个时间的竞争,这里用
lastCallTime
来记录最后一次debounced
调用时的时间,用lastInvokeTime
来记录最后一次func
被调用时的时间。lastInvokeTime
在invokeFunc
里保存:lastCallTime
在debounced
调用时保存:因为涉及到了
wait
和maxWait
的竞争,这时,我们就不能每次调用debounced
的时候都直接clearTimer
了和startTimer
了,因为这只是以wait
为维度的。因为
debounced
会多次调用,又不能每次都重置timer
,因此需要计算一个状态来是否开启timer
。以下为源码:
逐个条件分析下:
lastCallTime === undefined
,即第一次调用的时候timeSinceLastCall >= wait
,即事件触发的间隔超过wait
时,这是debounce
最开始就要支持的功能。timeSiceLastCall < 0
,这个从逻辑上很好理解,但是从场景上有点难理解,怎么会在下次检测的时候,time
会比lastCallTime
还要小的情况呢?难道有时光机?其实这种应该算是边缘情况,例如在修改系统时间的情况下。maxing && timeSinceLastInvoke >= maxWait
, 最后这个条件是处理最大等待时候的情况,如果timeSinceLastInvoke
比最大的等待时间maxWait
还要大,也要重新开启timer
。有了判断条件,还要计算
timer
的时间间隔,之前直接使用wait
,现在因为有了maxWait
,因此时间间隔也要使用这两者进行计算了。源码如下:
这个看其实很易理解,使用
wait - timeSinceLastCall
可以计算出没有maxWait
的时候的等待时间。如果要支持最大的等待时候,可以使用
maxWait - timeSinceLastInvoke
得出最大的等待时间所剩余的时间。然后取这两者中较小者就可以得出
timer
所要求的时间间隔了。因为
wait
肯定不会大于maxWait
,因此我们在第一次进入的时候调用startTimr
时传入的时间间隔应为wait
,但是这时传给timer
的回调函数不能直接是func
,因为这样会忽略掉maxWait
的情况。在这里,我们使用一个
timerExpired
函数来进行timer
的重启工作。源码如下:
如果在
wait
时间过后,shouldInvkoe
为false
则表示还没达到调用func
的条件,这里需要考虑maxWait
的情况,因此再次调用startTimer
,这次传入的时间为remainingWait(time)
,即重新计算后的等待时间来重启timer
。如果达到调用
func
的条件,直接调用func
即可。debounced
函数也需要作如下的修改:在调用
debounced
函数时,如果isInvoking
为true
,我们先忽略掉if(maxing)
这个分支,理解起来就很简单了,如果isInvoking
为true
,并且当前timerId
为undefined
,表示需要启动timer
了,直接调用startTimer
即可。if(maxing)
这个分支一开始我也不太理解,后来看到这篇文章《探究防抖(debounce)和节流(throttle)》和看了对应的测试用例之后,才知道原因。这篇文章写得挺详细,摘录如下:
汇总一下代码:
启用定时器前先调用
func
按照现在的实现方式,会在两个事件的触发时间大于
wait
或者等待的时间大于maxWait
时才会调用func
。但是如果没有设置
maxWait
,而事件的触发时间间隔一直小于wait
,就可能会导致func
在很长的时间内得不到触发,这时会希望有一个开关,可以控制在设置timer
前先调用func
,这样func
至少有一次调用的机会。这个开关同样也放在
options
中,用leading
来表示。获取值:
我们假设有一个函数
leadingEdge
来判断是否需要调用func
,先不管这个函数的具体实现,则debounced
函数需要修改如下:这很易理解,因为
timerId
为undefined
,又处于需要启动timer
的状态,所以需要调用leadingEdge
。现在来看看
leadingEdge
函数的实现:这里前两行是之前的逻辑,第三行就个判断,如果
leading
为true
,则马上调用invokeFunc
,不需要等待timer
跑完。定时器跑完后不调用
func
定时器跑完前可以用
leading
来控制是否调用func
,但是有时也想定时器跑完后不调用func
,目前的实现是定时器跑完后都调用func
的。同样在
options
中增加一个配置trailing
。然后从
trailing
中获取值。我们假设控制定时器跑完后是否调用
func
的逻辑在trailingEdge
中,则timerExpired
需要作如下修改:trailingEdge
的源码如下:原来
invokeFunc
里会重置lastArgs
和lastThis
,但是因为有trailing
这个开关,invokeFunc
可能会调用不到,因此在这里也将lastArgs
和lastThis
重置。这里除了
trailing
的判断外还有lastArgs
的判断,我们先不管lastArgs
在那里设置为undefined
,从代码直观上来看,没有lastArgs
调用invokeFunc
可能会出错,因为func
可能会得不到它想要的参数。其实后面会看到,手动调用
flush
函数的时候,可能会将lastArgs
设置为undefined
,如果timer
时间到的时候,这里invokeFunc
是不应该执行的,因此还没有获得它所需要的参数。其他方法
其实
debounce
到这里已经实现完毕了,但是debounce
还提供一些方法来供外界控制debounce
的内部进程,和查询进度,这些方法作为debounce
返回的函数debounced
的属性来提供给外界使用。cancel
cancel
方法其实在一开始的时候就已经实现,后来就不需要用到了,但是提供给外界可以取消timer
也是挺有用的:flush
flush
可以控制func
是否立即执行,不需要等待timer
时间到后再触发,源码如下:如果没有
timerId
表示func
已经执行过,或者第一次还没有调用,这里是没有lastArgs
的,直接返回上一次的结果result
即可。否则调用
traillingEdge
方法去调用func
,得到结果。因为手动调用了
traillingEdge
,会将timerId
设置为undefined
,这时如果再调用debounced
时,shouldInvoke
可能返回的是false
,但是因为已经调用过flush
,需要重新启动timer
。因此
debounced
也要修改如下:在
isInvoking
为false
时,检测timerId
是否为undefined
,如果是,则重新启动timer
,其实就是增加了这段代码:pending
pending
方法用来检测timer
是否正在运行中。源码如下:
如果
timerId
不为undefined
, 则表示timer
正在运行中。参考资料
探究防抖(debounce)和节流(throttle)
License
署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面
The text was updated successfully, but these errors were encountered: