Update: Thank you for your support, recently toss a summary of information, convenient for you to read the system, there will be more content and more optimization, click here to view

—— The following is the text ——

The introduction

In the last section, we learned how to implement the anti-shock and throttling functions in Lodash and took a look at the source code. In this article, we will continue to interpret the source code in a different way through seven small examples. The source code analysis of the article has been very detailed, here is no longer repeated, it is recommended that this article with the above take together, fierce stamp here to learn

If you have any thoughts or opinions, please leave them in the comments section.

Throttle function

Let’s start by looking at a diagram that illustrates the difference between Throttle and Debounce, and the different effects that can be produced under different configurations, where mousemove events fire every 50 ms, which is 50 ms per bit as shown in the figure below. Today’s article will start with the picture below.

Point 1

lodash.throttle(fn, 200, {leading: true, trailing: true})

Mousemove triggers for the first time

Take a look at the throttle source code

function throttle(func, wait, options) {
  // The first and last calls default to true
  let leading = true
  let trailing = true

  if (typeoffunc ! = ='function') {
    throw new TypeError('Expected a function')}// options is an object
  if (isObject(options)) {
    leading = 'leading' inoptions ? !!!!! options.leading : leading trailing ='trailing' inoptions ? !!!!! options.trailing : trailing }// maxWait is the wait function
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait,
  })
}
Copy the code

So throttle(fn, 200, {leading: true, trailing: true}) returns debounce(fn, 200, {leading: true, trailing: true, maxWait: 200}), add maxWait: 200.

As a precaution, we’ll get to the hard part, which is the Debounce entry function.

// the entry function returns this function
function debounced(. args) {
  // Get the current time
  const time = Date.now()
  // Determine whether the func function should be executed at this point
  const isInvoking = shouldInvoke(time)

  // Assigns values to closures for other function calls
  lastArgs = args
  lastThis = this
  lastCallTime = time

  / / execution
  if (isInvoking) {
    // There are two cases where there is no timerId:
    // first call
    // 2. TrailingEdge executes the function
    if (timerId === undefined) {
      return leadingEdge(lastCallTime)
    }
    
    // If the maximum wait time is set, func is executed immediately
    // 1. Start the timer and trigger trailingEdge when the time is up.
    // 2. Run the func command and return the result
    if (maxing) {
      // Calls are handled in the loop timer
      timerId = startTimer(timerExpired, wait)
      return invokeFunc(lastCallTime)
    }
  }
  // In a special case, trailing is set to true when trailingEdge of the previous wait has already executed
  // shouldInvoke returns false when called, so start the timer
  if (timerId === undefined) {
    timerId = startTimer(timerExpired, wait)
  }
  // Return the result when no execution is required
  return result
}
Copy the code

For debounce(fn, 200, {leading: true, trailing: true, maxWait: 200}), the following process is followed.

  • 1.shouldInvoke(time)C, because it satisfies this conditionlastCallTime === undefined, so return true
  • 2,lastCallTime = time, solastCallTimeIs equal to the current time, let’s say 0
  • 3,timerId === undefinedMeet, performleadingEdge(lastCallTime)methods
// Execute the callback at the beginning of the sequence
function leadingEdge(time) {
  // set the time when func was last executed
  lastInvokeTime = time
  // 2. Start the timer for the callback after the event ends
  timerId = startTimer(timerExpired, wait)
  // 3, if leading is configured, execute function func
  // leading from!! options.leading
  return leading ? invokeFunc(time) : result
}
Copy the code
  • 4, inleadingEdge(time)In the settinglastInvokeTimeIf the current time is 0, enable the 200 ms timer and runinvokeFunc(time)And return
// Execute the Func function
function invokeFunc(time) {
  // Get the parameter of the debmentioning last time
  const args = lastArgs
  // Get the last this
  const thisArg = lastThis

  / / reset
  lastArgs = lastThis = undefined
  lastInvokeTime = time
  result = func.apply(thisArg, args)
  return result
}
Copy the code
  • 5, ininvokeFunc(time),func.apply(thisArg, args), that is, the fn function executes for the first time and assigns the result toresultTo facilitate direct return when triggered. At the same time to resetlastInvokeTimeIf the current time is 0, clear itlastArgslastThis.
  • 6. The first trigger has been completedlastCallTimelastInvokeTimeBoth timers of 0,200 milliseconds are still running.

Mousemove Triggered the second time

50 milliseconds later, the second trigger occurs, and the current time is 50, wait 200, maxWait 200, Maxing true, lastCallTime and lastInvokeTime 0, and the timerId timer exists. Let’s look at the execution steps.

function shouldInvoke(time) {
  // The difference between the current time and the last time debounce was called
  const timeSinceLastCall = time - lastCallTime
  // The difference between the current time and the last time func was executed
  const timeSinceLastInvoke = time - lastInvokeTime

  // Return true for the following four cases
  return ( lastCallTime === undefined || 
          (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || 
          (maxing && timeSinceLastInvoke >= maxWait) )
}
Copy the code
  • 1.shouldInvoke(time),timeSinceLastCall50,timeSinceLastInvokeIf none of the four conditions are met, return false.
  • 2, at this time,isInvokingIs false, whiletimerId === undefinedIf not, return the first triggerresult
  • 3. When the second trigger is complete, fn will not be executed, only the result of the last execution will be returnedresult
  • 4, the third and fourth trigger, the effect is the same, will not repeat.

Mousemove triggers for the fifth time

Time 200, wait 200, maxWait 200, maxing true, lastCallTime 150, lastInvokeTime 0. The timerId timer exists. Let’s take a look at the execution steps.

  • 1.shouldInvoke(time),timeSinceLastInvokeIs 200, satisfied (maxing && timeSinceLastInvoke >= maxWait), so return true
// debmentioning method to execute this section
if (maxing) {
  // Calls are handled in the loop timer
  timerId = startTimer(timerExpired, wait)
  return invokeFunc(lastCallTime)
}
Copy the code
  • 2, meetmaxingCondition, restart the 200 ms timer and execute itinvokeFunc(lastCallTime)function
  • 3,invokeFunc(time)Reset,lastInvokeTimeIf the current time is 200, clearlastArgslastThis
  • 4, the sixth, seventh, eighth trigger, with the second trigger effect is consistent, it will not repeat.

Mousemove stops triggering

If the mousemove has stopped scrolling for the eighth time, the time is 350 on the eighth time, so if the mousemove has stopped firing for the ninth time, then fn should be executed. Will fn still be executed? The answer is still execute because {trailing: true} was originally set.

// Start the timer
function startTimer(pendingFunc, wait) {
  / / not wait at the window. The call transfer requestAnimationFrame ()
  if (useRAF) {
    // If you want to update the next frame of the animation before the browser redraws it
    / / callback function itself must again call window. RequestAnimationFrame ()
    root.cancelAnimationFrame(timerId);
    return root.requestAnimationFrame(pendingFunc)
  }
  // Start the timer when RAF is not in use
  return setTimeout(pendingFunc, wait)
}
Copy the code

The 200 ms timer is started on the fifth trigger, so pendingFunc will be executed when the time reaches 400. The pendingFunc is the timerExpired function.

// Timer callback function, indicating the operation after the timer ends
function timerExpired() {
  const time = Date.now()
  // 1. Determine whether to run the command
  // Execute the callback after the event, otherwise restart the timer
  if (shouldInvoke(time)) {
    return trailingEdge(time)
  }
  // 2. Otherwise, calculate the remaining waiting time and restart the timer to ensure that the end of the next delay is triggered
  timerId = startTimer(timerExpired, remainingWait(time))
}
Copy the code

In shouldInvoke(time), time is 400, lastInvokeTime is 200, timeSinceLastInvoke is 200, Satisfy (Maxing && timeSinceLastInvoke >= maxWait), so return true.

// Execute the callback after the end of a sequence of events
function trailingEdge(time) {
  // Empty the timer
  timerId = undefined

  // trailing and lastArgs exist at the same time
  // trailing source from 'trailing' in options? !!!!! options.trailing : trailing
  // The role of the lastArgs flag bit means that debounce has been executed at least once
  if (trailing && lastArgs) {
    return invokeFunc(time)
  }
  // Clear parameters
  lastArgs = lastThis = undefined
  return result
}
Copy the code

TrailingEdge (time) is then executed, in which the trailing and lastArgs conditions are both true, so invokeFunc(time) is executed, and finally the fn function is executed.

Two points need to be made here

  • If you set it up{trailing: false}, then the last time will not be executed. forthrottledebounceFor example, the default value is true, so if it is not specified specificallytrailing, then the last time will be executed.
  • forlastArgsIn terms of implementationdebouncedWill be reassigned, that is, every time it is triggered it will be reassigned, so when will it be cleared, wheninvokeFunc(time)Is reset to when the fn function is executed inundefined, so ifdebouncedIt only triggered once, even though it was set{trailing: true}Then fn will no longer be executed, which answers the first question left from the previous article.

Point 2

lodash.throttle(fn, 200, {leading: true, trailing: false})

{trailing: true} is the same as {trailing: true} if the trailing function is not set. The fn function is executed again after the event callback, but if {trailing: true} is set. False}, fn will not be executed after the event callback.

The difference with Angle 1 is that {trailing: false} is set, so there is no extra execution at the end, as shown in the first figure.

Point 3

lodash.throttle(fn, 200, {leading: false, trailing: true})

LeadingEdge (time) {leadingEdge(time) {leading: false}

// Execute the callback at the beginning of the sequence
function leadingEdge(time) {
  // set the time when func was last executed
  lastInvokeTime = time
  // 2. Start the timer for the callback after the event ends
  timerId = startTimer(timerExpired, wait)
  // 3, if leading is configured, execute function func
  // leading from!! options.leading
  return leading ? invokeFunc(time) : result
}
Copy the code

In this case, the 200 ms timer is started, and since leading is false, invokeFunc(time) is not executed, but result is returned, which is undefined.

The purpose of starting a timer here is for the callback after the end of the event, that is, if {trailing: true} is set, the last callback will execute the incoming function fn, even though the debmentioning function will only fire once.

{leading: false}} {leading: false} False for debounce and true for Throttle. So {leading: false} must be specified when throttle does not need to trigger initially, but not in debounce, which does not trigger by default.

Anti-shock function Debounce

Point 4

lodash.debounce(fn, 200, {leading: false, trailing: true})

For throttle, the maxWait value is missing, so the judgment in the trigger process is different. Let’s see in detail.

  • 1. In the entry functiondebounced,shouldInvoke(time)As discussed earlier, it returns true because it is fired the first time, and then executesleadingEdge(lastCallTime).
// Execute the callback at the beginning of the sequence
function leadingEdge(time) {
  // set the time when func was last executed
  lastInvokeTime = time
  // 2. Start the timer for the callback after the event ends
  timerId = startTimer(timerExpired, wait)
  // 3, if leading is configured, execute function func
  // leading from!! options.leading
  return leading ? invokeFunc(time) : result
}
Copy the code
  • 2, inleadingEdgebecauseleadingIs false, so fn is not executed, only the 200 ms timer is turned on, and returnsundefined. At this timelastInvokeTimeIs the current time, assuming 0.
// Determine whether the func function should be executed at this point
function shouldInvoke(time) {
  // The difference between the current time and the last time debounce was called
  const timeSinceLastCall = time - lastCallTime
  // The difference between the current time and the last time func was executed
  const timeSinceLastInvoke = time - lastInvokeTime

  // Return true for the following four cases
  return ( lastCallTime === undefined || 
          (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || 
          (maxing && timeSinceLastInvoke >= maxWait) )
}
Copy the code
  • 3. Each time it triggers,timeSinceLastCallIt’s always 50 milliseconds,maxingIs false, soshouldInvoke(time)Always return false. Fn is not executed, only result is returnedundefined.
  • 4. So far, FN has not been executed once. After 200 ms, the timer callback function is triggered and executedtimerExpiredfunction
// Timer callback function, indicating the operation after the timer ends
function timerExpired() {
  const time = Date.now()
  // 1. Determine whether to run the command
  // Execute the callback after the event, otherwise restart the timer
  if (shouldInvoke(time)) {
    return trailingEdge(time)
  }
  // 2. Otherwise, calculate the remaining waiting time and restart the timer to ensure that the end of the next delay is triggered
  timerId = startTimer(timerExpired, remainingWait(time))
}
Copy the code
  • 5. There are two situations. The first ismousemoveEvents are always firing, as described earliershouldInvoke(time)Returns false, and the remaining wait time is calculated and the timer restarts. The time calculation formula iswait - (time - lastCallTime)That’s 200 minus 50. So as long asshouldInvoke(time)Return false every 150 millisecondstimerExpired().
  • 6. The second case ismousemoveThe event no longer fires becausetimerExpired()The loop executes, so there must be a case to meettimeSinceLastCall >= wait, i.e.,shouldInvoke(time)Return true, terminatetimerExpired()Loop, and executetrailingEdge(time).
// Execute the callback after the end of a sequence of events
function trailingEdge(time) {
  // Empty the timer
  timerId = undefined

  // trailing and lastArgs exist at the same time
  // trailing source from 'trailing' in options? !!!!! options.trailing : trailing
  // The role of the lastArgs flag bit means that debounce has been executed at least once
  if (trailing && lastArgs) {
    return invokeFunc(time)
  }
  // Clear parameters
  lastArgs = lastThis = undefined
  return result
}
Copy the code
  • 7, intrailingEdgetrailinglastArgsBoth are true, so it will be executedinvokeFunc(time), that is, to execute the passed function fn.
  • 8. Therefore, only the last pass function fn is executed in the whole process, and the effect is as shown in the first picture above.

Point 5

lodash.debounce(fn, 200, {leading: true, trailing: false})

In contrast to Angle 4, the difference is {leading: true, trailing: false}, but wait and maxWait are identical to Angle 4, so there are only two differences, as shown in the first figure above.

  • The difference between 1:leadingEdgeThe passed function fn is executed in
  • The difference between 2:trailingEdgeThe passed function fn is no longer executed in

Point 6

lodash.debounce(fn, 200, {leading: true, trailing: true})

{leading: = {leading: = {leading: = {leading: = {leading: = {leading: = { True}, so there is only one difference. In leadingEdge, fn is passed in, but in trailingEdge, fn is passed in, so the mousemove event is triggered both at the beginning and at the end. The effect is the same as shown in the first image above.

Except, of course, that the Mousemove event only fires once, and the key is the lastArgs variable.

For the lastArgs variable, it will be evaluated in the entry function debmentioning, that is, every time it will be triggered it will be re-evaluated. When will it be clear, it will be reset to undefined in invokeFunc(time), so if debmentioning will trigger only once, If fn is executed once in {leading: true}, then {trailing: true} will not be executed again.

Point 7

lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})

Wait 200, maxWait 400, maxing true

  • 1. When triggered for the first time, because{leading: false}, so fn is definitely not executed, and a 200 ms timer is started.
// Determine whether the func function should be executed at this point
function shouldInvoke(time) {
  // The difference between the current time and the last time debounce was called
  const timeSinceLastCall = time - lastCallTime
  // The difference between the current time and the last time func was executed
  const timeSinceLastInvoke = time - lastInvokeTime

  // Return true for the following four cases
  return ( lastCallTime === undefined || 
          (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || 
          (maxing && timeSinceLastInvoke >= maxWait) )
}
Copy the code
  • 2. After that, it will be triggered every 50 milliseconds and executed every timeshouldInvoke(time)Function is satisfied only at the 400th millisecondmaxing && timeSinceLastInvoke >= maxWait, returns true.
// Calculate the waiting time
function remainingWait(time) {
  // The difference between the current time and the last time debounce was called
  const timeSinceLastCall = time - lastCallTime
  // The difference between the current time and the last time func was executed
  const timeSinceLastInvoke = time - lastInvokeTime
  // Remaining waiting time
  const timeWaiting = wait - timeSinceLastCall

  // Whether the maximum waiting time is set
	// Yes (throttling) : Returns the minimum value of "remaining wait time" and "remaining wait time since last func execution"
	// No: Returns the remaining waiting time
  return maxing
    ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
  	: timeWaiting
}
Copy the code
  • But at the 200th millisecond before that, the timer triggers the callback and executestimerExpiredBecause at this timeshouldInvoke(time)Returns false, so the remaining wait time is recalculated and the timer restarts, wheretimeWaitingIt’s 150 milliseconds,maxWait - timeSinceLastInvokeIt’s 200 milliseconds, so it’s 150 milliseconds.
  • After 150 ms, i.e. 350th ms since the start, the time is recalculated, wheretimeWaitingIt’s still 150 milliseconds,maxWait - timeSinceLastInvokeIs 50 ms, so restart the 50 ms timer, which is triggered at 400th ms.
  • 5. It will be found that the timer is triggered in the 400th ms.shouldInvoke(time)Returns true in the 400th millisecond. Is that a conflict? First, the remaining time of the timer is judged andshouldInvoke(time)In the judgment, as long as there is a point that meets the conditions for executing FN, it will be executed immediately, and at the same timelastInvokeTimeThe value will also change, so the other judgment won’t work. In addition, the timer itself is not accurate, so throughMath.min(timeWaiting, maxWait - timeSinceLastInvoke)Minimize the error.
  • 6. At the same time, we shall add the following sentence in the debmentioning entry functionif (timerId === undefined) {timerId = startTimer(timerExpired, wait)}To avoidtrailingEdgeThe timer is cleared after being executed.
  • 7. The final effect is the same as throttling, but with a larger interval, as shown in the first picture.

The previous answer

The first question

Q: If the leading and trailing options are true, how many times will the debc function be called during the wait, one time or two times? Why?

The answer is 1. Why? Detailed answers have been given in the paper. Please refer to Angles 1 and 6 for details.

The second question

Q: How do I pass arguments to func in debounce(func, time, options)?

In the first solution, since the debmentioning function can accept parameters, it can pass the parameters in the way of higher order function, as follows

const params = 'muyiy';
const debounced = lodash.debounce(func, 200)(params)
window.addEventListener('mousemove', debounced);
Copy the code

However, this method is not very friendly, because params will overwrite the original event, and then the scroll or mousemove event object will not be available.

The second approach, which is handled on a listener function, uses a closure to save the parameters passed in and return the function that needs to be executed.

function onMove(param) {
    console.log('param:', param);  // muyiy
  
    function func(event) {
      console.log('param:', param);  // muyiy
      console.log('event:', event);  // event
    }
    return func;
}
Copy the code

Use as follows

const params = 'muyiy';
const debounced = lodash.debounce(onMove(params), 200)
window.addEventListener('mousemove', debounced);
Copy the code

reference

Debounce and Throttle functions, and debounce source code appreciation for LoDash

Recommended reading

【 Advanced stage 6-3 】 Shallow throttling function throttle

The anti – shake function is debounce

Application of Throttle and Debounce in React

Further advanced in 6-6 】 【 article | ali P6 will Lodash stabilization throttling function principle