Lodash source code debounce function analysis

A, use,

In LoDash we can use the Debounce function for both shock rejection and flow rejection. I haven’t really noticed this before, but the debounce function in LoDash is incredibly dual-use, so today I’m going to briefly explain how it works.

Debounce in Lodash

// Case 1: When the resize event is triggered, calculateLayout execution will not be triggered immediately, but only when the density is greater than 150. That's what we call the anti-shake function;
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 

// Case 2: when clicked 'sendMail' is then called. Subsequent triggers within 300 milliseconds will be ignored. Triggers after 300 milliseconds will take effect and a new cycle will be calculated. This is what we call a throttling function;
jQuery(element).on('click', _.debounce(sendMail, 300, {
  'leading': true.'trailing': false
}));


Copy the code

Through the above analysis, we can find that in the same function, to achieve the effect of shaking and throttling;

Second, source code analysis

Let’s take a look at some of the preparation code;


const freeGlobal = typeof global= = ='object' && global! = =null && global.Object === Object && global;
// This is used to determine the current environment; Actually in the browser environment; Global point to the window;

Copy the code

The requestAnimationFrame API is used in preference to wait cases; Otherwise, use setTimeout;


constuseRAF = (! wait && wait ! = =0 && typeof root.requestAnimationFrame === 'function');

Copy the code

The entire Debounce function has the following structure; Closures include a subset of global variables; It then returns a function that executes logic based on the closure’s global variables; In fact, a lot of the higher-order functions that we’ve written have this idea;


function debounce (a , b){
  let c,d,e;
  return function (){
    let res = use(c,d,e);
    // todo something...}}Copy the code

The idea behind throttling in LoDash is this; Provide two closure variables leading, trailing; Whether the leading control is a throttling function; Trailing control is an anti-shake function; Here we discuss the idea of throttling; Must be returned by the function has been triggered, so we need to do is, the trigger for the first time, direct execution, then triggered directly ignored until after more than scheduled waiting time, trigger to perform, effectively will trigger, trigger a sparse become certain cycle, this is the so-called throttling;

So we need to implement a function first; Be able to determine whether the current function should be implemented; The function can be written like this;


function shouldInvoke (time) {
  const timeSinceLastCall = time - lastCallTime;
  const timeSinceLastInvoke = time - lastInvokeTime;
  return (lastCallTime === undefined || (timeSinceLastCall >= wait))
}

Copy the code

It can mean that the current time is compared to the last time it triggered each time. If the difference is greater than the predetermined period, then it can be executed. This function will be executed either for the first time, when undefined was triggered last time;

The overall code could look like this;


function debounce (func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime;

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  if (typeoffunc ! = ='function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  if(isObject(options)) { leading = !! options.leading trailing ='trailing' inoptions ? !!!!! options.trailing : trailing }function invokeFunc (time) {  // invoke: invoke;
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time  // Every time this function is executed, the global lastInvokeTime is updated to the time when the function is executed;
    result = func.apply(thisArg, args)  // This points to the global this, and the argument list points to lastArgs;
    return result
  }

  function leadingEdge (time) {
    lastInvokeTime = time
    return leading ? invokeFunc(time) : result
  }

  function shouldInvoke (time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    return (lastCallTime === undefined || (timeSinceLastCall >= wait))
  }


  function debounced (. args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time) // true

    lastArgs = args
    lastThis = this
    lastCallTime = time
    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
    }
    return result
  }
  return debounced
}

Copy the code

Therefore, the idea of throttling is actually very simple, that is, for each trigger per unit time, if the conditions are met, call, do not meet the conditions, ignore, the key to meet the conditions is whether the time difference between the current call time and the first trigger time is greater than the predetermined period;

Three, anti – shake function

The anti – shake function in LoDash is more difficult to write, the general idea of our implementation, can be like this;

Define a timer to execute within the wait time, and then execute the function registered by the timer if the trigger frequency is greater than wait. If not, clear the currently defined timer; A timer with a wait period starting from the trigger time; Equivalent to continuously advancing a wait period timer; That’s fine; But that’s not how LoDash is implemented; It is the timer that starts a wait period when triggered for the first time; Then every time trigger, do not clear the timer, but save the trigger time; Then, after a wait period, determine whether to execute the registered function. If the time difference is greater than or equal to wait, execute the function directly. If the time difference is greater than or equal to wait, execute the function directly. If the time difference is greater than or equal to wait, define a timer for the difference and check again. To achieve the purpose of shaking; At its core are the following paragraphs;


function leadingEdge (time) {
  // Reset any `maxWait` timer.
  lastInvokeTime = time
  // Start the timer for the trailing edge.
  timerId = startTimer(timerExpired, wait)
  // Invoke the leading edge.
  return leading ? invokeFunc(time) : result
}


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

function startTimer (pendingFunc, wait) {
  if (useRAF) {
    root.cancelAnimationFrame(timerId)
    return root.requestAnimationFrame(pendingFunc)
  }
  return setTimeout(pendingFunc, wait)
}


function remainingWait (time) {
  const timeSinceLastCall = time - lastCallTime
  const timeSinceLastInvoke = time - lastInvokeTime
  const timeWaiting = wait - timeSinceLastCall // Mainly this one

  return maxing
    ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
    : timeWaiting
}

Copy the code

4. Complete code


function debounce (func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime;

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  constuseRAF = (! wait && wait ! = =0 && typeof root.requestAnimationFrame === 'function') // See if the current environment can use requestAnimationFrame

  if (typeoffunc ! = ='function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  if(isObject(options)) { leading = !! options.leading maxing ='maxWait' in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait // maxWait and the one with the larger wait in the configuration item;
    trailing = 'trailing' inoptions ? !!!!! options.trailing : trailing }function invokeFunc (time) {  // invoke: invoke;
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time  // Every time this function is executed, the global lastInvokeTime is updated to the time when the function is executed;
    result = func.apply(thisArg, args)  // This points to the global this, and the argument list points to lastArgs;
    return result
  }
  // Start a task;
  function startTimer (pendingFunc, wait) {
    if (useRAF) {
      root.cancelAnimationFrame(timerId)
      return root.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  // End a task;
  function cancelTimer (id) {
    if (useRAF) {
      return root.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  function leadingEdge (time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time
    // Start the timer for the trailing edge.
    timerId = startTimer(timerExpired, wait)
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result
  }

  function remainingWait (time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  function shouldInvoke (time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

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

  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
  }

  function cancel () {
    if(timerId ! = =undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush () {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function pending () {
    returntimerId ! = =undefined
  }

  function debounced (. args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time) // true
    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
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}


Copy the code