lodash源码解析之debounce

1,326 阅读6分钟

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; 在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函数中后面的条件判断,请看下面的流程图: 其中最主要的就是在startTimer中,传入的timerExpired,因为是直接将timerExpired方法添加进定时器的,而在timerExpired方法中满足条件时,返回的是trailingEdge函数的执行结果,在trailingEdge函数中,满足条件又返回的是invokeFunc函数的执行结果,在invokeFunc函数中,就执行了func.apply(thisArg args),至此,执行了传入的函数(有种俄罗斯套娃的感觉),达到了防抖的目的