debounce实现 js_聊聊lodash的debounce实现

debounce实现 js_聊聊lodash的debounce实现本文同步自我的Blog前段时间团队内部搞了一个代码训练营,大家组织在一起实现lodash的throttle和debounce,实现起来觉得并不麻烦,但是最后和官方的一对比,发现功能的实现上还是有差距的,为了寻找我的问题,把官方源码阅读了一遍,本文是我阅读完成后的一篇总结。本文只会列出比较核心部分的代码和注释,如果对全部的源码有兴趣的欢迎直接看我的repo:什么是throttle和debo…

大家好,又见面了,我是你们的朋友全栈君。

本文同步自我的Blog

前段时间团队内部搞了一个代码训练营,大家组织在一起实现 lodash 的 throttle 和 debounce,实现起来觉得并不麻烦,但是最后和官方的一对比,发现功能的实现上还是有差距的,为了寻找我的问题,把官方源码阅读了一遍,本文是我阅读完成后的一篇总结。

本文只会列出比较核心部分的代码和注释,如果对全部的源码有兴趣的欢迎直接看我的repo:

什么是throttle和debounce

throttle(又称节流)和debounce(又称防抖)其实都是函数调用频率的控制器,这里只做简单的介绍,如果想了解更多关于这两个定义的细节可以看下后文给出的一张图片,或者阅读一下lodash的文档。

throttle:将一个函数的调用频率限制在一定阈值内,例如 1s 内一个函数不能被调用两次。

debounce:当调用函数n秒后,才会执行该动作,若在这n秒内又调用该函数则将取消前一次并重新计算执行时间,举个简单的例子,我们要根据用户输入做suggest,每当用户按下键盘的时候都可以取消前一次,并且只关心最后一次输入的时间就行了。

lodash 对这两个函数又增加了一些参数,主要是以下三个:

leading,函数在每个等待时延的开始被调用

trailing,函数在每个等待时延的结束被调用

maxwait(debounce才有的配置),最大的等待时间,因为如果 debounce 的函数调用时间不满足条件,可能永远都无法触发,因此增加了这个配置,保证大于一段时间后一定能执行一次函数

这里直接剧透一下,其实 throttle 就是设置了 maxwait 的 debounce,所以我这里也只会介绍 debounce 的代码,聪明的读者们可以自己思考一下为什么。

我的实现与lodash的区别

我自己的代码实现放在我的repo里,大家有兴趣的可以看下。之前说过我的实现和 lodash 有些区别,下面就用两张图来展示一下。

这是我的实现

debounce实现 js_聊聊lodash的debounce实现

这是lodash的实现

debounce实现 js_聊聊lodash的debounce实现

这里看到,我的代码主要有两个问题:

throttle 的最后一次函数会执行两次,而且并非稳定复现。

throttle 里函数执行的顺序不对,虽然我的功能实现了,但是对于每一次 wait 来说,我都是执行的 leading 那一次

lodash 的实现解读

下面,我就会带着这几个问题去看看 lodasah 的代码。

官方代码的实现也不是很复杂,这里我贴出一些核心部分代码和我阅读后的注释,后面会讲一下 lodash 的大概流程:

function debounce(func, wait, options) {

let lastArgs,

lastThis,

maxWait,

result,

timerId,

lastCallTime

// 参数初始化

let lastInvokeTime = 0 // func 上一次执行的时间

let leading = false

let maxing = false

let trailing = true

// 基本的类型判断和处理

if (typeof func != ‘function’) {

throw new TypeError(‘Expected a function’)

}

wait = +wait || 0

if (isObject(options)) {

// 对配置的一些初始化

}

function invokeFunc(time) {

const args = lastArgs

const thisArg = lastThis

lastArgs = lastThis = undefined

lastInvokeTime = time

result = func.apply(thisArg, args)

return result

}

function leadingEdge(time) {

// Reset any `maxWait` timer.

lastInvokeTime = time

// 为 trailing edge 触发函数调用设定定时器

timerId = setTimeout(timerExpired, wait)

// leading = true 执行函数

return leading ? invokeFunc(time) : result

}

function remainingWait(time) {

const timeSinceLastCall = time – lastCallTime // 距离上次debounced函数被调用的时间

const timeSinceLastInvoke = time – lastInvokeTime // 距离上次函数被执行的时间

const timeWaiting = wait – timeSinceLastCall // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置

// 两种情况

// 有maxing:比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间

// 无maxing:在下一次trailing时执行 timerExpired

return maxing

? Math.min(timeWaiting, maxWait – timeSinceLastInvoke)

: timeWaiting

}

// 根据时间判断 func 能否被执行

function shouldInvoke(time) {

const timeSinceLastCall = time – lastCallTime

const timeSinceLastInvoke = time – lastInvokeTime

// 几种满足条件的情况

return (lastCallTime === undefined //首次

|| (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait

|| (timeSinceLastCall < 0) //系统时间倒退

|| (maxing && timeSinceLastInvoke >= maxWait)) //超过最大等待时间

}

function timerExpired() {

const time = Date.now()

// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器

if (shouldInvoke(time)) {

return trailingEdge(time)

}

// 重启定时器,保证下一次时延的末尾触发

timerId = setTimeout(timerExpired, remainingWait(time))

}

function trailingEdge(time) {

timerId = undefined

// 有lastArgs才执行,意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行

if (trailing && lastArgs) {

return invokeFunc(time)

}

// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次

// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次

lastArgs = lastThis = undefined

return result

}

function cancel() {}

function flush() {}

function pending() {}

function debounced(…args) {

const time = Date.now()

const isInvoking = shouldInvoke(time) //是否满足时间条件

lastArgs = args

lastThis = this

lastCallTime = time //函数被调用的时间

if (isInvoking) {

if (timerId === undefined) { // 无timerId的情况有两种:1.首次调用 2.trailingEdge执行过函数

return leadingEdge(lastCallTime)

}

if (maxing) {

// Handle invocations in a tight loop.

timerId = setTimeout(timerExpired, wait)

return invokeFunc(lastCallTime)

}

}

// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;

// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行

if (timerId === undefined) {

timerId = setTimeout(timerExpired, wait)

}

return result

}

debounced.cancel = cancel

debounced.flush = flush

debounced.pending = pending

return debounced

}

这里我用文字来简单描述一下流程:

首次进入函数时因为 lastCallTime === undefined 并且 timerId === undefined,所以会执行 leadingEdge,如果此时 leading 为 true 的话,就会执行 func。同时,这里会设置一个定时器,在等待 wait(s) 后会执行 timerExpired,timerExpired 的主要作用就是触发 trailing。

如果在还未到 wait 的时候就再次调用了函数的话,会更新 lastCallTime,并且因为此时 isInvoking 不满足条件,所以这次什么也不会执行。

时间到达 wait 时,就会执行我们一开始设定的定时器timerExpired,此时因为time-lastCallTime < wait,所以不会执行 trailingEdge。

这时又会新增一个定时器,下一次执行的时间是 remainingWait,这里会根据是否有 maxwait 来作区分:

如果没有 maxwait,定时器的时间是 wait – timeSinceLastCall,保证下一次 trailing 的执行。

如果有 maxing,会比较出下一次 maxing 和下一次 trailing 的最小值,作为下一次函数要执行的时间。

最后,如果不再有函数调用,就会在定时器结束时执行 trailingEdge。

我的问题出在哪?

那么,回到上面的两个问题,我的代码究竟是哪里出了问题呢?

为什么顺序图不对

研究了一下,lodash是比较稳定的在trailing时触发前一次函数调用的,而我的则是每次在 maxWait 时触发的下一次调用。问题就出在对于定时器的控制上。

因为在编码时考虑到定时器和 maxwait 会冲突的问题,在函数每次被调用的时候都会 clearTimeout(timer),因此我的 trailing 判断其实只对整个执行流的最后一次有效,而非 lodash 所说的 trailing 控制的是函数在每个 wait 的最后执行。

而 lodash 并不会清除定时器,只是每次生成新的定时器的时候都会根据 lastCallTime 来计算下一次该执行的时间,不仅保证了定时器的准确性,也保证了对每次 trailing 的控制。

为什么最后会触发两次

通过打 log 我发现这种触发两次的情况非常凑巧,最后一次函数执行的时候,正好满足前一个时延的 trailing,然后自己这个 wait 的定时器也触发了,所以最后又触发了一次本次时延的 trailing,所以触发了两次。

理论上 lodash 也会出现这种情况,但是它在每次函数执行的时候都会删除 lastArgs 和 lastThis,而下次函数执行的时候都会判断这两个参数是否存在,因此避免了这种情况。

总结

其实之前就知道 debounce 和 throttle 的用途和含义,但是每次用起来都得去看一眼文档,通过这次自己实现以及对源码的阅读,终于做到了了熟于心,也发现自己的代码设计能力还是有缺陷,一开始并没有想的很到位。

写代码的,还是要多写,多看;慢慢做到会写,会看;与大家共勉。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/151586.html原文链接:https://javaforall.net

(0)
上一篇 2022年6月20日 上午6:46
下一篇 2022年6月20日 上午6:46


相关推荐

  • gitlab 删除分支 合并分支,合并时候冲突解决办法 笔记

    gitlab 删除分支 合并分支,合并时候冲突解决办法 笔记删除分支合并分支 如果合并存在冲突解决办法 当冲突的时候 点击在本地解决或者 checkout 会弹出一个这样的框 我想把 1112 合并到 docking 的分支上 步骤一 拉取远处的分支 并且创建新的分支 1112 来源于 1112 如果提示已经存在 直接执行步骤二 切换到 docking 并且把 1112 的代码合并 这时候如果你是在 vscode 下 合并的 会有明显

    2026年3月17日
    2
  • 医疗大数据平台的主流解决方案和设计

    医疗大数据平台的主流解决方案和设计  从价值服务提供的角度来看,这是一个需要对来自多源异构(时间序列)数据进行高效处理提供各种公共能力的资源池,也是促使用户/患者与医护人员及机构进行服务提供和安全可靠交互的控制中心。其主要特征包括平台的开放性、模块化、灵活性和可扩展性等,它肩负着多重重要的角色:一是需要支持多种可穿戴设备和数据类型的标准化接入,并具备大规模接入并发处理能力;必须尽量采用国内外标准化组织及行业通用的标准化协议和规…

    2022年5月8日
    52
  • 算法的时间复杂度和空间复杂度计算

    算法的时间复杂度和空间复杂度计算1、算法时间复杂度1.1算法时间复杂度的定义:    在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度,是一种“渐进表示法”。…

    2022年5月14日
    44
  • 完整教程:利用GPT-4生成勒索软件代码的LLM驱动型恶意软件MalTerminal现世

    完整教程:利用GPT-4生成勒索软件代码的LLM驱动型恶意软件MalTerminal现世

    2026年3月15日
    3
  • java8 stream流操作的flatMap(流的扁平化)

    java8 stream流操作的flatMap(流的扁平化)flatMap的用法和含义住要通过一个案例来讲解,案例:对给定单词列表["Hello","World"],你想返回列表["H","e","l","o","W","r","d"]第一种方式String[]words=newString[]{"Hello","World&quot

    2022年6月2日
    46
  • 浅谈ArrayList动态扩容

    浅谈ArrayList动态扩容环境:eclipse,jdk1.8简介ArrayList实现了List接口,继承了AbstractList,底层是数组实现的,一般我们把它认为是可以自增扩容的数组。它是非线程安全的,一般多用于单线程环境下(与Vector最大的区别就是,Vector是线程安全的,所以ArrayList性能相对Vector会好些),它实现了Serializable接口,因此它支持序列化,能够通过序列化传输

    2022年6月10日
    33

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号