We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
debounce 和 throttle 相信大家并不陌生,我猜想过去,FEer 对它们的了解大概分为以下几个阶段:
debounce
throttle
lodash
当然,在第三个阶段的人应该占绝大多数,当我还在第三阶段的时候,就希望有一篇技术文章,能让我一下就能达到最后一个阶段。结果就是我 naive 了,google 了许多资料,50%在反复地聊基本实现,20%在基础上聊了两者的区别,20%在聊 underscore 的实现,剩下10%很粗暴地把源码和注释贴了上来。这就让我很难受了,没办法,万事开头难,我只能将这些资料和源码结合起来,事半功倍地进行探索。事实也证明,一口是吃不成胖子的,所以这篇文章旨在拆分 lodash 的实现,一步一步地理解并缩短 第四阶段 到 第五阶段 的时间,至于之前处于前三阶段的同学,可以去找些其他的文章来进行学习。
underscore
什么是 debounce ?
debounce: Grouping a sudden burst of events (like keystrokes) into a single one. 防抖:将一组例如按下按键这种密集的事件归并成一个单独事件。
什么是 throttle ?
throttle: Guaranteeing a constant flow of executions every X milliseconds. 节流:保证每 X 毫秒恒定地执行一些操作。
为什么要重提一下两者的概念呢?因为我在第三阶段的时候,一直是把这两者分开理解的,等到理解了 lodash 的源码之后,才发现 throttle 是 debounce的一种特殊情况。如果从上面的看不出来的话,可以通俗地这么理解: debounce 将密集触发的事件合并成一个单独事件(不限时间,你可以一直密集地触发,它最终只会触发一次)而 throttle 在 debounce 的基础上增加了时间限制(maxWait),也就是你一直密集地触发时间,但是到了限定时间,它一定要触发一次,也就是上文中提到的 a constant flow of executions 。
maxWait
a constant flow of executions
可以照着这个 可视化分析界面 理解一下。
如果还没用过 lodash 的同学,建议先看下 lodash 里 debounce 和 throttle 的用法:
上图是一个最基本的 debounce 实现,下面我们来按照 lodash 的实现思路,进行 第一步 拆解。
为了后续的扩展实现,第一步我们将一个基本的 debounce 拆分为五个部分。
this
args
setTimeout
wait
经过上面的拆分,其实一个基本可用的 debounce 函数已经实现好了,但是我们会发现一个问题,他的调用严重依赖于 setTimeout,那么延迟时间是否一定为 wait 呢?其实是不一定的。
举个例子,比如说 wait 为 5,此时在某一个定时器的回调函数 timeExpired 检测到上一次触发时间的 lastCallTime 为 100,而 Date.now() 为 103,此时虽热 103 - 100 = 3 < 5,要开启下一次定时,但这个时候定时的时间为 5 - 3 = 2 就可以了。
5
timeExpired
lastCallTime
100
Date.now()
103
103 - 100 = 3 < 5
5 - 3 = 2
接下来,就要进行定时时间的优化。
对应完整源码以及 Demo:debounce-1
为了达到对定时时间的优化,我们需要加入时间参数进行详细计算,分为以下几步:
debounced
var lastCallTime // 缓存的上一个执行 debounced 的时间
/**辅助函数的缓存 */ now = Date.now
func
shouldInvoke
remainingWait
修改后的回调函数不再是单纯的调用 invokeFunc,而是先判断执行回调的时刻是否能够调用 func,如果可以,直接调用;如果不行,计算出真正的延迟时间并重置定时器。
invokeFunc
对应完整源码以及 Demo:debounce-2
为了之后 lodash 的功能扩展以及 throttle 的实现,这一步加入参数 最大限制时间 maxWait。分为以下几步:
lastInvokeTime
var lastInvokeTime = 0, // 缓存的上一个 执行 invokeFunc 的时间
max
min
nativeMax = Math.max, nativeMin = Math.min
options
if (isObject(options)) { maxing = 'maxWait' in options maxWait = maxing ? nativeMax(+options.maxWait || 0, wait) : maxWait }
(maxing && timeSinceLastInvoke >= maxWait) // 等待时间超过最大等待时间
还记得开头说的 throttle 只是一个 debounce 的特殊情况吗?准确的说这一步就增加了这个特殊情况(maxWait),那么我们就可以实现一个基本的 throttle了。
function debounce(func, wait, options) { // ...... } function throttle(func, wait) { return debounce(func, wait, { maxWait: wait }) }
对应完整源码以及 Demo:
trailing
trailingEdge
一般一些基础实现的 debounce ,在解决完 this 的指向 和 event 对象 时,紧接就要处理 前置执行 和 后置执行 的问题。在 lodash 里,将这两个操作分为 leading 和 trailing 两个参数,分别对应控制 leadingEdge 和 trailingEdge 两个工具函数的执行,这里我们先实现 trailing 。分为以下几步:
leading
leadingEdge
var trailing = true
trailing = 'trailing' in options ? !!options.trailing : trailing
// setTimeout 定时器的回调函数 function timeExpired() { // ...... if (canInvoke) { return trailingEdge(time) } // ...... }
这一步基本和上一步类似,分为以几步:
var leading = false
leading = !!options.leading
// 要返回的包装 debounce 操作的函数 function debounced() { // ...... if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime) } // ...... } // ...... }
至此,一个基本完整的 debounce 和 throttle 已经实现了,下一步只是锦上添花,加一些额外的 feature。
feature
cancel
flush
在 lodash 的实现里,还增加了两个贴心的小功能,这里也一并贴上来:
// 取消 debounce 函数 function cancel() { if (timerId !== undefined) { clearTimeout(timerId) } lastInvokeTime = 0 lastArgs = lastCallTime = lastThis = timerId = undefined }
// 取消并立即执行一次 debounce 函数 function flush() { return timerId === undefined ? result : trailingEdge(now()) }
虽然一开始直接撕源码,觉得有点小复杂,但是只要将其主干剥离之后再理逻辑,就会将难度减少很多。从上述分步过程来看 lodash 的总体实现,总体可以分为
debounced()
fomrtArgs()
startTimer(time)
timeExpired()
shouldInvoke(time)
invokeFunc(time)
leadingEdge(time)
trailingEdge(time)
isObject(value)
remainingWait(time)
cancel()
flush()
以下是我整理的一个执行流程图(完整大图在 repo 里),可以照着参考一下
篇幅有限,难免一些错误,欢迎探讨和指教~ 附一个 GitHub 完整的 repo 地址: https://github.com/LazyDuke/debounce-throttle-exploring
GitHub
这是一个系列,系列文章:
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
debounce
和throttle
相信大家并不陌生,我猜想过去,FEer 对它们的了解大概分为以下几个阶段:lodash
这种稍微复杂一点实现的当然,在第三个阶段的人应该占绝大多数,当我还在第三阶段的时候,就希望有一篇技术文章,能让我一下就能达到最后一个阶段。结果就是我 naive 了,google 了许多资料,50%在反复地聊基本实现,20%在基础上聊了两者的区别,20%在聊
underscore
的实现,剩下10%很粗暴地把源码和注释贴了上来。这就让我很难受了,没办法,万事开头难,我只能将这些资料和源码结合起来,事半功倍地进行探索。事实也证明,一口是吃不成胖子的,所以这篇文章旨在拆分lodash
的实现,一步一步地理解并缩短 第四阶段 到 第五阶段 的时间,至于之前处于前三阶段的同学,可以去找些其他的文章来进行学习。一些必须知道的
什么是
debounce
?什么是
throttle
?为什么要重提一下两者的概念呢?因为我在第三阶段的时候,一直是把这两者分开理解的,等到理解了
lodash
的源码之后,才发现throttle
是debounce
的一种特殊情况。如果从上面的看不出来的话,可以通俗地这么理解:debounce
将密集触发的事件合并成一个单独事件(不限时间,你可以一直密集地触发,它最终只会触发一次)而throttle
在debounce
的基础上增加了时间限制(maxWait
),也就是你一直密集地触发时间,但是到了限定时间,它一定要触发一次,也就是上文中提到的a constant flow of executions
。可以照着这个 可视化分析界面 理解一下。
如果还没用过
lodash
的同学,建议先看下lodash
里debounce
和throttle
的用法:分步实现
debounce
上图是一个最基本的
debounce
实现,下面我们来按照lodash
的实现思路,进行 第一步 拆解。第一步 —— 基础的拆解
为了后续的扩展实现,第一步我们将一个基本的
debounce
拆分为五个部分。没有什么好说的,一个健壮的工具函数是少不了入参校验的,当然,在第一步只是实现了最基本的校验和格式化。
和基础实现一样,最后的结果是返回一个包装了所有操作的函数,可以看到,里面的实现和基础实现类似,不同的是这里多了一步记录上一次调用的
this
和args
。将
setTimeout
设置定时器操作语义化为一个函数,入参是wait
将回调函数抽成一个函数,目前的操作只有 invoke 需要防抖的函数,后续会慢慢添加功能。
调用需要防抖的函数,这里做了一个参数的传递,获取
this
和args
。经过上面的拆分,其实一个基本可用的
debounce
函数已经实现好了,但是我们会发现一个问题,他的调用严重依赖于setTimeout
,那么延迟时间是否一定为wait
呢?其实是不一定的。接下来,就要进行定时时间的优化。
对应完整源码以及 Demo:debounce-1
第二步 —— 对定时时间的优化
为了达到对定时时间的优化,我们需要加入时间参数进行详细计算,分为以下几步:
debounced
函数的时间lastCallTime
func
的工具函数shouldInvoke
remainingWait
timeExpired
修改后的回调函数不再是单纯的调用
invokeFunc
,而是先判断执行回调的时刻是否能够调用func
,如果可以,直接调用;如果不行,计算出真正的延迟时间并重置定时器。对应完整源码以及 Demo:debounce-2
第三步 —— 加入
maxWait
,实现基本的throttle
为了之后
lodash
的功能扩展以及throttle
的实现,这一步加入参数 最大限制时间maxWait
。分为以下几步:invokeFunc
函数的时间lastInvokeTime
max
和min
options
的校验remainingWait
shouldInvoke
的判断条件debounced
的执行过程还记得开头说的
throttle
只是一个debounce
的特殊情况吗?准确的说这一步就增加了这个特殊情况(maxWait
),那么我们就可以实现一个基本的throttle
了。对应完整源码以及 Demo:
第四步 —— 增加入参选项
trailing
以及trailingEdge
工具函数一般一些基础实现的
debounce
,在解决完 this 的指向 和 event 对象 时,紧接就要处理 前置执行 和 后置执行 的问题。在lodash
里,将这两个操作分为leading
和trailing
两个参数,分别对应控制leadingEdge
和trailingEdge
两个工具函数的执行,这里我们先实现trailing
。分为以下几步:trailing
设置默认值trailing
的校验和格式化trailingEdge
invokeFunc
,而是通过trailingEdge
来间接调用对应完整源码以及 Demo:
第五步 —— 增加入参选项
leading
以及leadingEdge
工具函数这一步基本和上一步类似,分为以几步:
leading
设置默认值leading
的校验和格式化leadingEdge
debounced
的执行过程至此,一个基本完整的
debounce
和throttle
已经实现了,下一步只是锦上添花,加一些额外的feature
。对应完整源码以及 Demo:
第六步 —— 增加
cancel
和flush
功能在
lodash
的实现里,还增加了两个贴心的小功能,这里也一并贴上来:debounce
效果的cancel
debounce
函数的flush
对应完整源码以及 Demo:
总结
虽然一开始直接撕源码,觉得有点小复杂,但是只要将其主干剥离之后再理逻辑,就会将难度减少很多。从上述分步过程来看
lodash
的总体实现,总体可以分为debounced()
fomrtArgs()
startTimer(time)
timeExpired()
shouldInvoke(time)
invokeFunc(time)
leadingEdge(time)
trailingEdge(time)
isObject(value)
和 计算真正延迟时间的函数remainingWait(time)
debounce
效果的cancel()
和 取消并立即执行一次debounce
函数的flush()
)以下是我整理的一个执行流程图(完整大图在 repo 里),可以照着参考一下
篇幅有限,难免一些错误,欢迎探讨和指教~
附一个
GitHub
完整的 repo 地址: https://github.com/LazyDuke/debounce-throttle-exploring后记
这是一个系列,系列文章:
The text was updated successfully, but these errors were encountered: