博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
由浅入深学习lodash的debounce函数
阅读量:5977 次
发布时间:2019-06-20

本文共 4131 字,大约阅读时间需要 13 分钟。

最近的面试中考到了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))

可以看出,只有在最后一次点击的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才是真正的实现了。

转载地址:http://ykpox.baihongyu.com/

你可能感兴趣的文章
Redis实现分布式锁2
查看>>
【Udacity】线性回归方程 Regression
查看>>
前端架构设计1:代码核心
查看>>
RPC 框架通俗解释 转自知乎(洪春涛)
查看>>
获取cookie后,使用cookie进行接下来的自动化操作
查看>>
算法笔记--数论模板小集(待增)
查看>>
SASS初学者入门(转)
查看>>
C语言100个算法经典例题(七)
查看>>
轻松实现远程批量拷贝文件脚本(女学生作品)
查看>>
Nmap在pentest box中的扫描及应用
查看>>
测试组合索引
查看>>
四、物理优化(2)索引视图
查看>>
【沟通之道】头脑风暴-女人的心思你别猜
查看>>
redux-form(V7.4.2)笔记(一)
查看>>
钱趣多风控新举措:源头选择与物理隔离
查看>>
puppet最新源码包安装学习笔记
查看>>
烂泥:kickstart无人值守安装CentOS6.5
查看>>
Windows Phone 8 开发资源汇总
查看>>
互联网趋势关键词:交流,为价值付费,资源整合
查看>>
阿里钉钉,马云旗下的又一个千亿美金产品?
查看>>