-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
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
【进阶 7-5 期】浅出篇 7 个角度吃透 Lodash 防抖节流原理 #44
Comments
yygmind
changed the title
【进阶 6-7 期】浅出篇 7 个角度吃透 Lodash 防抖节流原理
【进阶 7-5 期】浅出篇 7 个角度吃透 Lodash 防抖节流原理
Sep 29, 2019
博主怎么断更了? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
引言
上一节我们学习了 Lodash 中防抖和节流函数是如何实现的,并对源码浅析一二,今天这篇文章会通过七个小例子为切入点,换种方式继续解读源码。其中源码解析上篇文章已经非常详细介绍了,这里就不再重复,建议本文配合上文一起服用,猛戳这里学习
有什么想法或者意见都可以在评论区留言,欢迎大家拍砖。
节流函数 Throttle
我们先来看一张图,这张图充分说明了 Throttle(节流)和 Debounce(防抖)的区别,以及在不同配置下产生的不同效果,其中
mousemove
事件每 50 ms 触发一次,即下图中的每一小隔是 50 ms。今天这篇文章就从下面这张图开始介绍。角度 1
lodash.throttle(fn, 200, {leading: true, trailing: true})
mousemove 第一次触发
先来看下 throttle 源码
所以
throttle(fn, 200, {leading: true, trailing: true})
返回内容是debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
,多了maxWait: 200
这部分。先打个预防针,后面即将开始比较难的部分,看下 debounce 入口函数。
对于
debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
来说,会经历如下过程。shouldInvoke(time)
中,因为满足条件lastCallTime === undefined
,所以返回 truelastCallTime = time
,所以lastCallTime
等于当前时间,假设为 0timerId === undefined
满足,执行leadingEdge(lastCallTime)
方法leadingEdge(time)
中,设置lastInvokeTime
为当前时间即 0,开启 200 毫秒定时器,执行invokeFunc(time)
并返回invokeFunc(time)
中,执行func.apply(thisArg, args)
,即 fn 函数第一次执行,并把结果赋值给result
,便于后续触发时直接返回。同时重置lastInvokeTime
为当前时间即 0,清空lastArgs
和lastThis
。lastCallTime
和lastInvokeTime
都为 0,200 毫秒的定时器还在运行中。mousemove 第二次触发
50 毫秒后第二次触发到来,此时当前时间
time
为 50,wait
为 200,maxWait
为 200,maxing
为 true,lastCallTime
和lastInvokeTime
都为 0,timerId
定时器存在,我们来看下执行步骤。shouldInvoke(time)
中,timeSinceLastCall
为 50,timeSinceLastInvoke
为 50,4 种条件都不满足,返回 false。isInvoking
为 false,同时timerId === undefined
不满足,直接返回第一次触发时的result
result
mousemove 第五次触发
距第一次触发 200 毫秒后第五次触发到来,此时当前时间
time
为 200,wait
为 200,maxWait
为 200,maxing
为 true,lastCallTime
为 150,lastInvokeTime
为 0,timerId
定时器存在,我们来看下执行步骤。shouldInvoke(time)
中,timeSinceLastInvoke
为 200,满足(maxing && timeSinceLastInvoke >= maxWait)
,所以返回 truemaxing
条件,重新开启 200 毫秒的定时器,并执行invokeFunc(lastCallTime)
函数invokeFunc(time)
中,重置lastInvokeTime
为当前时间即 200,清空lastArgs
和lastThis
mousemove 停止触发
假设第八次触发之后就停止了滚动,在第八次触发时
time
为 350,所以如果有第九次触发,那么此时是应该执行fn 的,但是此时 mousemove 已经停止了触发,那么还会执行 fn 吗?答案是依旧执行,因为最开始设置了{trailing: true}
。在第五次触发时开启了 200 毫秒的定时器,所以在时间
time
到 400 时会执行pendingFunc
,此时的pendingFunc
就是timerExpired
函数,来看下具体的代码。此时在
shouldInvoke(time)
中,time
为 400,lastInvokeTime
为 200,timeSinceLastInvoke
为 200,满足(maxing && timeSinceLastInvoke >= maxWait)
,所以返回 true。之后执行
trailingEdge(time)
,在这个函数中判断trailing
和lastArgs
,此时这两个条件都是 true,所以会执行invokeFunc(time)
,最终执行函数 fn。这里需要说明以下两点
{trailing: false}
,那么最后一次是不会执行的。对于throttle
和debounce
来说,默认值是 true,所以如果没有特意指定trailing
,那么最后一次是一定会执行的。lastArgs
来说,执行debounced
时会赋值,即每次触发都会重新赋值一次,那什么时候清空呢,在invokeFunc(time)
中执行 fn 函数时重置为undefined
,所以如果debounced
只触发了一次,即使设置了{trailing: true}
那也不会再执行 fn 函数,这个就解答了上篇文章留下的第一道思考题。角度 2
lodash.throttle(fn, 200, {leading: true, trailing: false})
在「角度 1 之 mousemove 停止触发」这部分中说到,如果不设置
trailing
和设置{trailing: true}
效果是一样的,事件回调结束后都会再执行一次传入函数 fn,但是如果设置了{trailing: false}
,那么事件回调结束后是不会再执行 fn 的。此时的配置对比角度 1 来说,区别在于设置了
{trailing: false}
,所以实际效果对比 1 来说,就是最后不会额外再执行一次,效果见第一张图。角度 3
lodash.throttle(fn, 200, {leading: false, trailing: true})
此时的配置和角度 1 相比,区别在于设置了
{leading: false}
,所以直接看leadingEdge(time)
方法就可以了。在这里,会开启 200 毫秒的定时器,同时因为
leading
为 false,所以并不会执行invokeFunc(time)
,只会返回result
,此时的result
值是undefined
。这里开启一个定时器的目的是为了事件结束后的那次回调,即如果设置了
{trailing: true}
那么最后一次回调将执行传入函数 fn,哪怕debounced
函数只触发一次。这里指定了
{leading: false}
,那么leading
的初始值是什么呢?在debounce
中是 false,在throttle
中是 true。所以在throttle
中不需要刚开始就触发时,必须指定{leading: false}
,在debounce
中就不需要了,默认不触发。防抖函数 Debounce
角度 4
lodash.debounce(fn, 200, {leading: false, trailing: true})
此时相比较 throttle 来说,缺少了
maxWait
值,所以具体触发过程中的判断就不一样了,来详细看一遍。debounced
中,执行shouldInvoke(time)
,前面讨论过因为第一次触发所以会返回 true,之后执行leadingEdge(lastCallTime)
。leadingEdge
中,因为leading
为 false,所以并不执行 fn,只开启 200 毫秒的定时器,并返回undefined
。此时lastInvokeTime
为当前时间,假设为 0。timeSinceLastCall
总是为 50 毫秒,maxing
为 false,所以shouldInvoke(time)
总是返回 false,并不会执行传入函数 fn,只返回 result,即为undefined
。timerExpired
函数mousemove
事件一直在触发,根据前面介绍shouldInvoke(time)
会返回 false,之后就将计算剩余等待时间,重启定时器。时间计算公式为wait - (time - lastCallTime)
,即 200 - 50,所以只要shouldInvoke(time)
返回 false,就每隔 150 毫秒后执行一次timerExpired()
。mousemove
事件不再触发,因为timerExpired()
在循环执行,所以肯定会存在一种情况满足timeSinceLastCall >= wait
,即shouldInvoke(time)
返回 true,终结timerExpired()
的循环,并执行trailingEdge(time)
。trailingEdge
中trailing
和lastArgs
都是 true,所以会执行invokeFunc(time)
,即执行传入函数 fn。角度 5
lodash.debounce(fn, 200, {leading: true, trailing: false})
此时相比角度 4 来说,差异在于
{leading: true, trailing: false}
,但是wait
和maxWait
都和角度 4 一致,所以只存在下面 2 种区别,效果同上面第一张图所示。leadingEdge
中会执行传入函数 fntrailingEdge
中不再执行传入函数 fn角度 6
lodash.debounce(fn, 200, {leading: true, trailing: true})
此时相比角度 4 来说,差异仅仅在于设置了
{leading: true}
,所以只存在一个区别,那就是在leadingEdge
中会执行传入函数 fn,当然在trailingEdge
中依旧执行传入函数 fn,所以会出现在 mousemove 事件触发过程中首尾都会执行的情况,效果同上面第一张图所示。当然一种情况除外,那就是
mousemove
事件永远只触发一次的情况,关键在于lastArgs
变量。对于
lastArgs
变量来说,在入口函数debounced
中赋值,即每次触发都会重新赋值一次,那什么时候清空呢,在invokeFunc(time)
中重置为undefined
,所以如果debounced
只触发了一次,而且在{leading: true}
时执行过一次 fn,那么即使设置了{trailing: true}
也不会再执行传入函数 fn。角度 7
lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})
此时
wait
为 200,maxWait
为 400,maxing
为 true,我们来看下执行过程。{leading: false}
,所以肯定不会执行 fn,此时开启了一个 200 毫秒的定时器。shouldInvoke(time)
函数,只有在第 400 毫秒时,才会满足maxing && timeSinceLastInvoke >= maxWait
,返回 true。timerExpired
,因为此时shouldInvoke(time)
返回 false,所以会重新计算剩余等待时间并重启计时器,其中timeWaiting
是 150 毫秒,maxWait - timeSinceLastInvoke
是 200 毫秒,所以计算结果是150 毫秒。timeWaiting
依旧是 150 毫秒,maxWait - timeSinceLastInvoke
是 50 毫秒,所以重新开启 50 毫秒的定时器,即在第 400 毫秒时触发。shouldInvoke(time)
中返回 true 的时间也是在第 400 毫秒,为什么要这样呢?这样会冲突吗?首先定时器剩余时间判断和shouldInvoke(time)
判断中,只要有一处满足执行 fn 条件,就会立马执行,同时lastInvokeTime
值也会发生改变,所以另一处判断就不会生效了。另外本身定时器是不精准的,所以通过Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
取最小值的方式来减少误差。if (timerId === undefined) {timerId = startTimer(timerExpired, wait)}
,避免trailingEdge
执行后定时器被清空。上期答疑
第一题
问:如果
leading
和trailing
选项都是 true,在wait
期间只调用了一次debounced
函数时,总共会调用几次func
,1 次还是 2 次,为什么?答案是 1 次,为什么?文中已给出详细解答,详情请看角度 1 和角度 6。
第二题
问:如何给
debounce(func, time, options)
中的func
传参数?第一种方案,因为
debounced
函数可以接受参数,所以可以用高阶函数的方式传参,如下不过这种方式不太友好,params 会将原来的 event 覆盖掉,此时就拿不到 scroll 或者 mousemove 等事件对象 event 了。
第二种方案,在监听函数上处理,使用闭包保存传入参数并返回需要执行的函数即可。
使用时如下
参考
推荐阅读
❤️ 看完三件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
The text was updated successfully, but these errors were encountered: