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
, solastCallTime
Is equal to the current time, let’s say 0 - 3,
timerId === undefined
Meet, 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, in
leadingEdge(time)
In the settinglastInvokeTime
If 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, in
invokeFunc(time)
,func.apply(thisArg, args)
, that is, the fn function executes for the first time and assigns the result toresult
To facilitate direct return when triggered. At the same time to resetlastInvokeTime
If the current time is 0, clear itlastArgs
和lastThis
. - 6. The first trigger has been completed
lastCallTime
和lastInvokeTime
Both 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)
,timeSinceLastCall
50,timeSinceLastInvoke
If none of the four conditions are met, return false. - 2, at this time,
isInvoking
Is false, whiletimerId === undefined
If 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 returned
result
- 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)
,timeSinceLastInvoke
Is 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, meet
maxing
Condition, restart the 200 ms timer and execute itinvokeFunc(lastCallTime)
function - 3,
invokeFunc(time)
Reset,lastInvokeTime
If the current time is 200, clearlastArgs
和lastThis
- 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. forthrottle
和debounce
For example, the default value is true, so if it is not specified specificallytrailing
, then the last time will be executed. - for
lastArgs
In terms of implementationdebounced
Will 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 ifdebounced
It 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 function
debounced
,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, in
leadingEdge
becauseleading
Is false, so fn is not executed, only the 200 ms timer is turned on, and returnsundefined
. At this timelastInvokeTime
Is 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,
timeSinceLastCall
It’s always 50 milliseconds,maxing
Is 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 executed
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
- 5. There are two situations. The first is
mousemove
Events 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 is
mousemove
The 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, in
trailingEdge
中trailing
和lastArgs
Both 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:
leadingEdge
The passed function fn is executed in - The difference between 2:
trailingEdge
The 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 time
shouldInvoke(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 executes
timerExpired
Because at this timeshouldInvoke(time)
Returns false, so the remaining wait time is recalculated and the timer restarts, wheretimeWaiting
It’s 150 milliseconds,maxWait - timeSinceLastInvoke
It’s 200 milliseconds, so it’s 150 milliseconds. - After 150 ms, i.e. 350th ms since the start, the time is recalculated, where
timeWaiting
It’s still 150 milliseconds,maxWait - timeSinceLastInvoke
Is 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 timelastInvokeTime
The 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 function
if (timerId === undefined) {timerId = startTimer(timerExpired, wait)}
To avoidtrailingEdge
The 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