最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • lodash源码解析之debounce

    正文概述 掘金(callme靓仔)   2021-02-27   1669

    debounce也就是防抖函数,什么是防抖,防抖主要就是用在用户在一定事件内多次触发事件的时候,只执行一次,其实现原理主要是用定时器的机制实现,下面看看lodash中的debounce函数

    lodash中的debounce函数接受三个参数:func, wait, options 1、func就是需要做防抖处理的函数 2、wait就是延迟多长时间触发 3、options配置项,可有maxwait-最大等待时长等参数 最终返回的是一个 debounced函数,debounce 具体代码:

    function debounced(...args) {
      const time = Date.now()
      const isInvoking = shouldInvoke(time)
      
      lastArgs = args
      lastThis = this
      lastCallTime = time
    
      if (isInvoking) {
        if (timerId === undefined) {
          return leadingEdge(lastCallTime)
        }
        if (maxing) {
          // Handle invocations in a tight loop.
          timerId = startTimer(timerExpired, wait)
          return invokeFunc(lastCallTime)
        }
      }
      if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait)
      }
      return result
    }
    

    说一下其中出现的函数: 1、shouldInvoke函数,这个函数源码是这样的

    function shouldInvoke(time) {
      const timeSinceLastCall = time - lastCallTime
      const timeSinceLastInvoke = time - lastInvokeTime
      // Either this is the first call, activity has stopped and we're at the
      // trailing edge, the system time has gone backwards and we're treating
      // it as the trailing edge, or we've hit the 'maxWait' limit.
      return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
        (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
    }
    

    直接看返回结果,

    1、lastCallTime === undefined表示是第一次触发,因为lastCallTime默认值就是undefined;lodash源码解析之debounce 在debounce函数的开头,定义了这一些变量

    2、timeSinceLastCall >= wait 表示两次触发的间隔已经超过了wait,也就是可以再次触发;

    3、lastInvokeTime 在 invokeFunc 中被赋值为lastCallTime(后面解析invokeFunc函数),所以timeSinceLastCall < 0 几乎不可能出现,在第一次触发,然后调整过计算机时间时可能会出现 timeSinceLastCall < 0 的情况;

    4、maxing为真表示使用防抖函数时,传入了options参数,切options参数中有maxwait属性;timeSinceLastInvoke >= maxWait,lastInvokeTime初始值为undefined,后被赋值为lastCallTime,所以timeSinceLastInvoke >= maxWait也表示两次触发的时间间隔超过了最大等待时长

    所以在初次进入到debounced函数中,会进入到if(isInvoking)条件判断内(后面触发防抖的话,就不会进入条件判断了),timerId在第一次触发时,因为定义时未赋值,所以为undefined,代码继续往下执行,调用了leadingEdge方法,并传入lastCallTime参数,现在看下leadingEdge方法:

    function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time
        // Start the timer for the trailing edge.
        timerId = startTimer(timerExpired, wait)
        // Invoke the leading edge.
        // 如果传入了optios并且options是个对象,leading就是 !!options.leading,否则,默认为false
        // 在invokeFunc中对result赋值过
        return leading ? invokeFunc(time) : result
    }
    

    很简单的逻辑,就是将lastInvokeTime赋值为lastCallTime(也就是time,为Date.now()),然后timerId赋值为startTimer(timerExpired, wait),看下startTimer函数内部:

    function startTimer(pendingFunc, wait) {
        if (useRAF) {
          // cancelAnimationFrame用于取消 requestAnimationFrame方法调用计划的动画帧请求
          root.cancelAnimationFrame(timerId)
          return root.requestAnimationFrame(pendingFunc)
        }
        return setTimeout(pendingFunc, wait)
    }
    

    在debounce函数的开头,有定义useRef,

    const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
    

    简单解释就是调用decounce函数时,如果未传入wait,或则传入的wait为0,root.requestAnimationFrame是一个函数(root为Window对象,所以root.requestAnimationFrame === 'function'即表示在浏览器环境下),此时useRef就为true;

    requestAnimationFrame和cancelAnimationFrame是浏览器的两个api,

    额外小知识:
    requestAnimationFrame官方解释:告诉浏览器,我需要执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画,这么做的目的就是,如果wait为0,那么定时器就不会延迟执行(这里不讨论宏任务和微任务的问题),也就是setTimeout(callfunc,0),我们都知道帧动画最低每秒60帧,再低就会看到卡顿的现象,也就是掉帧;浏览器也遵循这个规则,所以一帧大概是16ms左右,也就是浏览器每16ms重绘一次(现在浏览器可能会根据屏幕刷新率做出调整),所以定时器就算是延迟0s执行,浏览器也不会立即做出响应,还是要等重绘时才能看到dom的更新,而且定时器属于宏任务,会被浏览器添加进宏任务队列,遵循事件循环机制,所以一般定时器的延迟要高于其设定的延迟时间,所以requestAnimationFrame要优于定时器的,而cancelAnimationFrame也就是取消requestAnimationFrame提交的回调

    回归正题,timerId被赋值了,在看下调用startTimer函数时传入的timerExpired函数,其代码如下:

    function timerExpired() {
        const time = Date.now()
        if (shouldInvoke(time)) {
          return trailingEdge(time)
        }
        // Restart the timer.
        timerId = startTimer(timerExpired, remainingWait(time))
    }
    

    前面已经解读过shouldInvoke函数了,其值如果为true,返回一个新函数trailingEdge(time),否则就重置timerId的值,其中还涉及另外一个新的函数--remainingWait,先看下trailingEdge函数,代码如下:

    function trailingEdge(time) {
        timerId = undefined
    
        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time)
        }
        lastArgs = lastThis = undefined
        return result
    }
    

    直接看其注释的意思,只有在有lastArgs时才调用,这意味着func至少已经防抖过一次了。也就是func并不是第一次调用了,总结就是,未防抖过,就返回invokeFunc(time)的值,否则返回result。
    继续解读invokeFunc函数,其代码如下:

    function invokeFunc(time) {
        const args = lastArgs
        const thisArg = lastThis
    
        lastArgs = lastThis = undefined
        lastInvokeTime = time
        result = func.apply(thisArg, args)
        return result
    }
    

    其实也就是将使用apply改变func调用时的this指向,并将结果赋值给result

    再看remainingWait函数,其代码如下:

    function remainingWait(time) {
        // 在debounce中对 lastCallTime 赋值为当前时间
        const timeSinceLastCall = time - lastCallTime
        // lastInvokeTime 默认值为0,中途有赋值为time,看函数是否执行
        const timeSinceLastInvoke = time - lastInvokeTime
        const timeWaiting = wait - timeSinceLastCall
    
        return maxing
          ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting
    }
    

    直接看return后面的代码,前面已经说过maxing的来历,默认为false,赋值为maxing = 'maxWait' in options,timeWaiting = wait - timeSinceLastCall;timeSinceLastCall = time - lastCallTime;time为执行到timerExpired函数时从新取的Date.now(),而lastCallTime是在执行debounced函数时取的Date.now()(每次触发函数都会重新赋值的),所以它们两个之间是有一个时间差的,timeSinceLastCall就姑且理解为代码执行到现在所用的时间吧。
    timeSinceLastInvoke = time - lastInvokeTime,lastInvokeTime是在invokeFunc函数中为其赋值的,而invokeFunc函数执行的时候,也就是setTimeout执行的时候或则是requestAnimationFrame回调执行的时候,但是time是在debounced执行之初传入的,所以lastInvokeTime的值为第一次调用debounced函数时的时间戳,后续触发防抖的时候,并不会对lastInvokeTime重新赋值,直到func第一次执行完,也就是过完wait时间之后再次触发才会被重新赋值;所以timeSinceLastInvoke的值表示初次触发函数到现在的时间差

    回到leadingEdge函数中,其返回值为 return leading ? invokeFunc(time) : result,leading默认值为false,后被赋值为 !!options.leading。
    这里也就是要不要执行invokeFunc函数的问题,如果options中没有传leading参数,就不执行,result为undefined,否则执行invokeFunc函数,result被赋值为func.apply(...)

    至此,调用debounce时不传入options的情况已经解读完毕,所涉及到的函数也已经解读完,至于debounced函数中后面的条件判断,请看下面的流程图: lodash源码解析之debounce 其中最主要的就是在startTimer中,传入的timerExpired,因为是直接将timerExpired方法添加进定时器的,而在timerExpired方法中满足条件时,返回的是trailingEdge函数的执行结果,在trailingEdge函数中,满足条件又返回的是invokeFunc函数的执行结果,在invokeFunc函数中,就执行了func.apply(thisArg args),至此,执行了传入的函数(有种俄罗斯套娃的感觉),达到了防抖的目的


    起源地下载网 » lodash源码解析之debounce

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元