lodash
中的throttle
函数比较有意思,观察源码,会发现它本质是调用了一次debounce
实现的返回结果。并且该结果上还有cancel
和flush
两个方法可以使用。它们分别对应取消及立即调用该debounce
方法。
那我们先从debounce
看起,我们知道lodash
中debounce
入参的options
支持leading
和trailing
两种模式(默认不配置情况下,leading
为false
,trailing
为true
)。前者表明我们试图让这个被debounce
处理过的函数在定时器生效时,第一次就触发。后者就是我们传统理解上的当多次触发时,以最后一次触发为延后响应,常见场景如移动端窗口变化触发的resize
、输入框输入的搜索场景,触发ajax
。
所以一般我们自己实现的debounce
就是lodash
中默认配置入参模式下的debounce
:
1 | function debounce(fn, wait = 400) { |
表现效果就如下,上面一行表明我们触发的响应事件频率,下面一行表明真正函数被执行的时机。
P.S. 相关图片引用外网这篇《Debouncing and Throttling Explained Through Examples》。
上述的表现就是lodash
中的trailing
模式,即对后回调进行防抖处理。
那leading
模式又是什么?
在lodash
中,当我们配置leading
为true
,trailing
为false
,就会有下面的表现。
可以看到leading
模式下,同样具有防抖的机制,但是它的时机提前了,在每次触发开始就会执行(前提是已经超过了这个wait
时长)。
我们不妨自己思考下leading
如何实现,既然要让函数在最初执行,那我们就不能把执行的函数放到定时器里面,但是又需要保证这个防抖的机制怎么办呢?我的答案是设置一个哨兵变量进行是否立刻执行的判断并将哨兵变量放到防抖的定时里面去操作,结合闭包访问的能力,就能达到该效果,代码如下:
1 | function debounceWithLeadingOpt(fn, wait = 400) { |
下面我们不妨看看源码是怎么做的:
1 | function debounced(...args) { |
我们对大体结构进行解读,函数内部做了几件事:
- 获取当前时间,判断是否函数正在调用过程中
- 如果正在调用中,判断是否存在定时器,不存在则会通过
leadingEdge
进行lastInvokeTime
的更新和定时器生成。该方法内,在我们的leading
配置打开的场景下,会通过invokeFunc
立即执行我们要调用的函数,并更新对应的时间lastInvokeTime
。 - 存在定时器,则我们会先通过判断
maxing
变量判断是否用户在options
中配置了maxWait
,如果配置了,会生成一个定时器去执行timerExpired
。然后立即执行invokeFunc
调用函数。 - 如果不在调用中,且没有定时器,则初始化定时器。
参数初始化:
1 | let lastArgs, |
下面一个个看过来,先看shouldInvoke
:
1 | function shouldInvoke(time) { |
我们发现其中涉及到两个记录内部时间的变量,lastCallTime
和lastInvokeTime
,它们分别记录了上一次debounced
函数调用时间和invokeFunc
函数调用时间。
该函数返回一个boolean
,用于判断是否进行我们函数的invoke
。
下面看看leadingEdge
:
1 | function leadingEdge(time) { |
执行包装的目标函数invokeFunc
,并记录相关时间:
1 | function invokeFunc(time) { |
设置trailing
模式下的定时器任务函数startTimer
:
1 | // 在没有传wait的时候 使用rAF进行定时器处理 可以简单理解为16ms的setTimeout 只不过由浏览器接管 严格按照浏览器的frame去回调 |
trailing
模式设置的定时回调timerExpired
:
1 | // 计算剩余时长 |
trailing
时机触发时,执行对应包装目标函数:
1 | // 该方法比较关键,它并不代表在trailing时机去执行我们的函数 |
源码中的处理涉及到的环节比较多,因为其中包含了leading
及trailing
双模式。两者都有共同的处理流程。
如果只看leading
模式,并且不走trailing
触发,会有下面这样的流程(本质上我们外层消费的是返回出的debounced
函数):
根据流程图可以发现leading
有两个前置判断,除了定时器还会多做一层执行时机的计算判断。并且这个定时器会在后续的流程中进行更新和清空。总体而言比我们实现的要复杂不少,当然其兼容了两种模式,并且很多通道都可以复用,多也是正常的。
下面我们瞅瞅throttle
,它从源码上分析其实就是同时启用leading
、trailing
并且配置了maxWait
的debounce
函数,根据前面的源码和流程综合也好理解,就不进行冗余展开了:
1 | function throttle(func, wait, options) { |
最后我们在官方文档的docs说明和源码注释中都能看到,当leading
和trailing
都为true
时,必须在我们trailing
定时的wait
时间内至少触发一次deboungced
函数,才会在trailing
时机再次执行一次目标函数,因为从函数体我们可以看到还依赖了第二个条件参数lastArgs
,而这个参数只有在debounced
中会被赋予值。
到此源码阅读和表现分析已结束,以上。