资讯专栏INFORMATION COLUMN

由浅入深学习lodash的debounce函数

Raaabbit / 819人阅读

摘要:定时器调用频率优化把开启定时器的逻辑放在可以大大减少定时器的数量。举个例子,比如为,此时在某一个定时器的回调函数检测到上一次触法事件的为,而为,此时虽然要开启下一次定时,但这个时候定时的时间为就可以了。

最近的面试中考到了debounce,函数防抖,笔试的时候答的不是特别好,下来好好研究了一下,从原理到优化,再到开源工具库lodash的实现源码,梳理了一番,现整理如下。

先简单介绍一下debounce,从最简单的一个场景入手,当用户不断点击页面,短时间内频繁的触法点击事件,只有在用户触法事件后的ns时间内,没有再触法事件,真正的监听函数才会执行,如果在这段时间内再次触法了事件,就需要重新计算这个ns。

debounce最主要的作用是把多个触法事件的操作延迟到最后一次触法执行,在性能上做了一定的优化。

不使用debounce

如果不使用debounce,那就会每一次点击都会触法事件的回调函数,这有时候对于性能是一种巨大的浪费(比如大量的增加dom元素)。或者当回调函数计算量很大的时候,甚至会导致阻塞。

window.addEventListener("click", function (event) {
  var p = document.createElement("p")
  p.innerHTML = "trigger"
  document.body.appendChild(p)
})

频繁触法
可以看出,每一次点击都会触法函数执行。

使用debounce
window.addEventListener("click", debounce(function (event) {
    var p = document.createElement("p")
    p.innerHTML = "trigger"
    document.body.appendChild(p)
    return "aaaa"
}, 500))

debounce优化
可以看出,只有在最后一次点击的500ms后,真正的函数func才会触法。

开始实现debounce

本篇文章的debounce实现主要参考了lodash库,会从最基础的实现开始,一步步完善它。
debounce的核心实现,就是要判断每次触法事件的时候,要不要执行真正的func

大体思路就是每次触法事件都开启一个延时的定时器,在定时器结束的时候对比与最后一次触法事件时的时间差,如果时间差大于延迟的阈值,那么就执行真正的func`。

大致的结构如下

function debounce (func, wait) {
    var lastCallTime   // 最后一次触法事件的时间
    var lastThis       // 作用域
    var lastArgs       // 参数
    var timerId        // 定时器对象
    wait = +wait || 0
    // 启动定时器
    function startTimer (timerExpired, wait) {
        return setTimeout(timerExpired, wait)
    }
    
    // func函数执行   
    function invokeFunc () {
    
    }
    
    // 调用func函数的判定条件 
    function shouldInvoke () {
    
    }
    
    //  定时器的回调函数 
    function timerExpired () {
        // 在这里判断触法事件的时间差
    }
    
    // 要返回的函数
    function debounced (...args) {
    
    }
    
    return debounced
}

这就是基本的debounce函数的构成,下面边解析,边去一一填充这些函数,最后再对函数进行一步步的优化。

debounced

每一次触法事件的时候都会进入到这个函数,这个函数需要做这么几个事情。

确定作用域和参数

更新触法事件的时间,也就是lastCallTime

启动定时器 timerId

function debounced (...args) {
    const time = Date.now()
    lastThis = this
    lastArgs = args
    lastCallTime = time
    timerId = startTimer(timerExpired, wait)
}
startTimer

startTimer 就是启动一个定时器,后续会有更多的拓展,所以封装一个函数

function startTimer (timerExpired, wait) {
    return setTimeout(timerExpired, wait)
}
timerExpired

timerExpired 主要判断是否执行func

function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
}
shouldInvoke

shouldInvoke判断每次事件触法的时间差,如果大于阈值,那么真正的func就会执行

function shouldInvoke (time) {
    return lastCallTime !== undefined && (time - lastCallTime >= wait)
}
invokeFunc
function invokeFunc () {
    timerId = undefined
    const args = lastArgs
    const thisArg = lastThis
    let result = func.apply(thisArg, args)
    lastArgs = lastThis = undefined
    return result
}

这样,这个函数就写完了。把每一步拆解开来,理解还是相对容易的,再总结一下。每一次触法事件,都开启一个定时器timerId,并且会更新触法事件的最后时间lastCallTime,在定时器的回调函数里面,判断回调函数的执行时间与lastCallTime的时间差,如果大于阈值,说明延迟时间到了,func执行,如果小于,就忽略。

优化

虽然实现了基本的debounce,但在扩展它的功能之前,看一看有没有优化的空间,每一次触法事件都开启一个定时器是不是太浪费了。这里可不可以减少调用次数。

定时器调用频率优化

把开启定时器的逻辑放在timerExpired可以大大减少定时器的数量。debounced开启了第一次定时器后,debounced会忽略后面的定时器开启,直到func执行之后(timerIdundefined),而在timerExpired里面判断如果func不满足触发条件,那么就开启下一个定时器。

其实本质就是确保上一个定时器的回调不会触法func了,才会开启下一个定时器。

优化代码如下

function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
    timerId = startTimer(timerExpired, wait)
}
function debounced (...args) {
    const time = Date.now()
    lastThis = this
    lastArgs = args
    lastCallTime = time
    if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait)
    }
}
定时器时间的优化

timerExpired 中开启的定时器

timerId = startTimer(timerExpired, wait)

延迟的时间是否一定为wait呢,这是不一定的。
举个例子,比如wait5,此时在某一个定时器的回调函数timerExpired检测到上一次触法事件的lastCallTime100,而Date.now()103,此时虽然103-100 = 3 < 5,要开启下一次定时,但这个时候定时的时间为 5 - 3 = 2就可以了。这才是精确的时间。

所以我们需要把这个时间封装成一个函数remainingWait

function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeWaiting = wait - timeSinceLastCall
    return timeWaiting
}
function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
    timerId = startTimer(timerExpired, remainingWait(time))
}

附上执行的流程图

总结

这其实只是实现了一个basicDebounce,其实有的时候我们需要在频繁触法事件的开始立即执行func,而忽略后面的触法事件,这就需要加入参数控制,也就是lodash中的trailingleading,甚至两者同时存在,头尾各执行一次,还有就是throttle函数节流,保证在一段时间内func至少执行一次,这就是lodash中的maxWait参数。下一篇文章会完善这些功能,届时,一个完整的debounce才是真正的实现了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95495.html

相关文章

  • lodash源码学习节流与防抖

    摘要:首先重置防抖函数最后调用时间,然后去触发一个定时器,保证后接下来的执行。这就避免了手动管理定时器。   之前遇到过一个场景,页面上有几个d3.js绘制的图形。如果调整浏览器可视区大小,会引发图形重绘。当图中的节点比较多的时候,页面会显得异常卡顿。为了限制类似于这种短时间内高频率触发的情况,我们可以使用防抖函数。   实际开发过程中,这样的情况其实很多,比如: 页面的scroll事件 ...

    CloudDeveloper 评论0 收藏0
  • 【源码分析】给你几个闹钟,或许用 10 分钟就能写出 lodash debounce &

    摘要:最简单的案例以最简单的情景为例在某一时刻点只调用一次函数,那么将在时间后才会真正触发函数。后续我们会逐渐增加黑色闹钟出现的复杂度,不断去分析红色闹钟的位置。 序 相比网上教程中的 debounce 函数,lodash 中的 debounce 功能更为强大,相应的理解起来更为复杂; 解读源码一般都是直接拿官方源码来解读,不过这次我们采用另外的方式:从最简单的场景开始写代码,然后慢慢往源码...

    余学文 评论0 收藏0
  • [译]通过实例讲解Debouncing和Throtting(防抖与节流)

    摘要:译通过实例讲解和防抖与节流源码中推荐的文章,为了学习英语,翻译了一下原文链接作者本文来自一位伦敦前端工程师的技术投稿。首次或立即你可能发现防抖事件在等待触发事件执行,直到事件都结束后它才执行。 [译]通过实例讲解Debouncing和Throtting(防抖与节流) lodash源码中推荐的文章,为了学习(英语),翻译了一下~ 原文链接 作者:DAVID CORBACHO 本文来自一位...

    Jenny_Tong 评论0 收藏0
  • JS throttle与debounce区别

    摘要:可以看下面的栗子这个图中图中每个小格大约,右边有原生事件与节流去抖插件的与事件。即如果有连续不断的触发,每执行一次,用在每隔一定间隔执行回调的场景。执行啦打印执行啦打印执行啦节流按照上面的说明,节流就是连续多次内的操作按照指定的间隔来执行。 一般在项目中我们会对input、scroll、resize等事件进行节流控制,防止事件过多触发,减少资源消耗;在vue的官网的例子中就有关于lod...

    wawor4827 评论0 收藏0
  • 快速 TypeScript 化 lodash throttle & debounce

    摘要:背景需要包写起来爽,然而如果遇到没有现成的化的工具函数,就需要自己想办法弄出一份类型声明文件了。最为重要的是,这种迁移方面我们可以随意自定义化中所需要的工具函数,迁移粒度都可以由自己控制。 1、背景 1.1、需要 TS 包 TypeScript 写起来爽,然而如果遇到没有现成的 TS 化的工具函数,就需要自己想办法弄出一份类型声明文件了。 前两天要写的小工具库(Typescript 语...

    lewinlee 评论0 收藏0

发表评论

0条评论

Raaabbit

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<