Update: Thank you for your support. Recently, a blog official website came out to facilitate your system reading. There will be more content and more optimization in the future

—— The following is the text ——

The introduction

In the previous sections we learned about throttle and Debounce, and how to implement them in the React project. In this article we will discuss how Lodash and Lodash are implemented, as well as the source code. The next article will take a few small examples as a starting point, in another way to continue to interpret the source code, please look forward to.

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

Anti-shock function debounce

The throttling function in Lodash is relatively simple. It calls the anti-shock function directly, passes in some configuration and becomes a throttling function. So let’s take a look at how the anti-shock function is implemented first.

I will not introduce the definition and custom implementation of the anti – shake function, before writing a special article, poke here to learn

In the body of the text, let’s take a look at the debounce source code, which is not much, more than 100 lines, and list the code structure for ease of understanding, followed by a step-by-step introduction to the entry functions.

The code structure

function debounce(func, wait, options) {
  // Save some variables through closures
  let lastArgs, // Last time I will execute the arguments of debmentioning,
      					// acts as a marker bit for the trailingEdge method, cleared by invokeFunc
    lastThis, // Save this last time
    maxWait, // Maximum wait time, data from options, implement throttling effect, ensure that the execution will be able to exceed a certain time
    result, If func is executed multiple times but the func condition is not met, result is returned
    timerId, // setTimeout Specifies the generated timer handle
    lastCallTime // The last time debounce was called

  let lastInvokeTime = 0 // The last time func was executed, in conjunction with maxWait, is used for throttling correlation
  let leading = false // Whether to respond to the initial callback of the event, that is, the first time it was triggered
  let maxing = false // Whether there is a maximum wait time, with maxWait mostly used for throttling correlation
  let trailing = true // Whether to respond to the callback after the end of the event, that is, the last time it was triggered

  / / not wait at the window. The call transfer requestAnimationFrame ()
  / / window. RequestAnimationFrame () telling the browser wish to perform the animation and request the browser before the next redraw call the specified function to update the animation, almost 16 ms performs a
  constuseRAF = (! wait && wait ! = =0 && typeof root.requestAnimationFrame === 'function')

  // Make sure the input func is a function, otherwise an error is reported
  if (typeoffunc ! = ='function') {
    throw new TypeError('Expected a function')}// Type Number
  wait = +wait || 0
  
  // Get the configuration options passed in by the user
  if(isObject(options)) { leading = !! options.leading// Options if maxWait is available, the throttling function is reserved
    maxing = 'maxWait' in options
    // maxWait is the largest of the set maxWait and wait. If maxWait is smaller than wait, maxWait is meaningless
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' inoptions ? !!!!! options.trailing : trailing }// ----------- open/close timer -----------
  // Start the timer
  function startTimer(pendingFunc, wait) {}

  // Cancel the timer
  function cancelTimer(id) {}

  // Timer callback function, indicating the operation after the timer ends
  function timerExpired() {}
  
  // Calculate the waiting time
  function remainingWait(time) {}
  
  // ----------- executes the incoming function -----------
	// Execute the callback at the beginning of the sequence
  function leadingEdge(time) {}
  
  // Execute the callback after the end of a sequence of events
  function trailingEdge(time) {}

  // Execute the func function
  function invokeFunc(time) {}

  // Determine whether the func function should be executed at this point
  function shouldInvoke(time) {}

  // ----------- external 3 methods -----------
  // Cancel function delay
  function cancel() {}

  // Execute func immediately
  function flush() {}

  // Check whether the timer is currently running
  function pending() {}

  // ----------- entry function -----------
  function debounced(. args) {}
  
  // Bind method
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  
  // return the entry function
  return debounced
}
Copy the code

The debounce(func, wait, options) method provides three parameters, the first is the function we want to execute, the second is timeout wait, and the third is optional. They are leading, trailing and maxWait.

The entry function

The debounce function will finally return debounce, and the function returned will be the entry function. When the event is triggered, the debounce function will be executed frequently, so in this method, it will need to “decide whether the introducing function func should be implemented”, and then start the timer according to the conditions. That’s what the debmentioning function will do.

// 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

On/off timer

StartTimer and timerExpired methods have been used in the entry function for many times, which are related to timer and time calculation. Besides these two methods, there are also cancelTimer and remainingWait.

startTimer

When the event is triggered, set a timer with a specified timeout period and pass in a callback function. The callback function pendingFunc is actually timerExpired. There is a distinction between the use of requestAnimationFrame and the use of setTimeout.

// 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

cancelTimer

If the timer is on, it needs to be turned off naturally. Turning off is very simple, as long as it distinguishes RAF and non-RAF, and the time ID is passed in when canceling.

// Cancel the timer
function cancelTimer(id) {
  if (useRAF) {
    return root.cancelAnimationFrame(id)
  }
  clearTimeout(id)
}
Copy the code

timerExpired

The pendingFunc callback passed in the startTimer function is actually the timer callback timerExpired, indicating the operation after the timer expires.

After the timing ends, there are two cases, one is to execute the function func passed in, the other is not executed. In the first case, it is necessary to determine whether the incoming function func needs to be executed and execute the last callback if necessary. For the second method, the remaining waiting time is calculated and the timer is restarted to ensure that the end of the next delay is triggered.

// 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

remainingWait

Here we calculate the amount of time we still have to wait, and we use a lot of variables, nine of them, so let’s look at what each variable means.

  • Time Indicates the current time stamp
  • LastCallTime Time when debounce was last called
  • TimeSinceLastCall Time difference between the current time and the last time debounce was called
  • LastInvokeTime Time when func was last executed
  • TimeSinceLastInvoke The time difference between the current time of the Invoke and the last funC execution
  • Wait Indicates the wait time for input
  • TimeWaiting Remaining waiting time
  • MaxWait Indicates the maximum wait time. The data is obtained from options and reserved for the throttling function
  • Whether maxing sets the maximum waiting time is determined bymaxWait in options
  • maxWait - timeSinceLastInvokeTime remaining to wait since last func execution

The variable is really many, do not understand the suggestion to look again, of course, the core is the following part, according to Maxing judgment should return the specific remaining waiting time.

// Whether maxing 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

This section is core, and the complete code is commented below.

// 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

Executing the passed function

After talking about timer and time-related functions, this part of the source code parsing has been carried out for the most part, next we look at the logic of executing the passed function func, divided into the execution of the beginning of the callback leadingEdge, the execution of the end of the callback trailingEdge, Execute the invokeFunc function normally, and check whether shouldInvoke should be executed.

leadingEdge

There are three main steps in this method after the initial callback of the event is executed as soon as the event is triggered and no longer waits for the wait time.

  • Sets the last time func was executedlastInvokeTime
  • Start timer
  • Execute the passed function func
// 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

trailingEdge

This is the end of the event callback, the simple thing to do here is to execute the func function and clear the parameters.

// 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

invokeFunc

Having said that, how does the func function work? Apply (thisArg, args) and reset some parameters.

// 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

shouldInvoke

So when you do invokeFunc in the entry function, you’re going to decide whether you should do it or not, so let’s look at the logic in detail, just like in remainingWait, there are a lot of variables, so let’s review them.

  • Time Indicates the current time stamp
  • LastCallTime Time when debounce was last called
  • TimeSinceLastCall Time difference between the current time and the last time debounce was called
  • LastInvokeTime Time when func was last executed
  • TimeSinceLastInvoke The time difference between the current time of the Invoke and the last funC execution
  • Wait Indicates the wait time for input
  • MaxWait Indicates the maximum wait time. The data is obtained from options and reserved for the throttling function
  • Whether maxing sets the maximum waiting time is determined bymaxWait in options

Let’s take a step by step look at the core code for judgment. There are four types of logic.

return ( lastCallTime === undefined || 
       (timeSinceLastCall >= wait) ||
       (timeSinceLastCall < 0) || 
       (maxing && timeSinceLastInvoke >= maxWait) )
Copy the code

There are four cases in which true is returned.

  • lastCallTime === undefinedThe first time I call it
  • timeSinceLastCall >= waitWait to process the callback after the end of the event
  • timeSinceLastCall < 0Current time – The last call time is less than 0, that is, the system time has changed
  • maxing && timeSinceLastInvoke >= maxWaitThe maximum waiting time is exceeded
// 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 in all four cases
  return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
Copy the code

External 3 methods

The Debmentioning function provides three methods, namely cancel, Flush and pending, and provides attributes for binding in the following way.

// Bind method
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
Copy the code

cancel

This is called cancel execution, and the main thing to cancel is to clear the timer, then clear the necessary closure variables, and return to the original state.

// Cancel function delay
function cancel() {
  // Clear the timer
  if(timerId ! = =undefined) {
    cancelTimer(timerId)
  }
  // Clear closure variables
  lastInvokeTime = 0
  lastArgs = lastCallTime = lastThis = timerId = undefined
}
Copy the code

flush

This is the immediate-execute method provided externally, so that it can be called when needed.

  • Returns if no timer exists, meaning no event has been fired or the event has been executedresultThe results of
  • If a timer exists, run it immediatelytrailingEdgeAfter the execution is complete, the timer ID will be cleared.lastArgslastThis
// Execute func immediately
function flush() {
  return timerId === undefined ? result : trailingEdge(Date.now())
}
Copy the code

pending

Obtain the current status and check whether the current timer is being timed. The presence of timerId timerId indicates that the timer is being timed.

// Check whether the timer is currently running
function pending() {
  returntimerId ! = =undefined
}
Copy the code

Throttle function

The definition and custom implementation of the throttling function are not covered in this article

throttle

This part of the source code is relatively simple, compared to the anti-shake is only triggered by different conditions, to put it simply, maxWait for wait anti-shake function.

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

isObject()

IsObject is used to determine if it is an object. The principle is typeof value, which returns true if it isObject or function.

function isObject(value) {
  const type = typeof value
  returnvalue ! =null && (type == 'object' || type == 'function')}Copy the code

Let me give you a couple of little examples

isObject({})
// => true

isObject([1.2.3])
// => true

isObject(Function)
// => true

isObject(null)
// => false
Copy the code

To consider

The source code analysis has been completed, do you really understand, leave a few questions for everyone, welcome to answer, the answer will be given in the next article.

  • ifleadingtrailingAll options are true inwaitOnly called once duringdebouncedFunction is called several times in totalfuncOnce or twice. Why?
  • How to givedebounce(func, time, options)In thefuncThe parameters?

reference

Learn throttling and anti – shake from LoDash source code

Recommended reading

【 Advanced stage 6-3 】 Shallow throttling function throttle

The anti – shake function is debounce

Application of Throttle and Debounce in React

❤️ Read three things

If you find this article inspiring, I’d like to invite you to do me three small favors:

  1. Like, so that more people can see this content (collection does not like, is a rogue -_-)
  2. Follow me on GitHub and make it a long-term relationship
  3. Pay attention to the public number “advanced front-end advanced”, focus on conquering a front-end interview every week, the public number back to send you selected front-end quality information.