1. Why should functions be throttled
There is the following code
let n = 1
window.onmousemove = (a)= > {
console.log(The first `${n}Trigger callback ')
n++
}
Copy the code
When we slide the mouse on a PC page, we can trigger about 60 events per second. You can also check out the online examples below.
Check out the online example: Function throttling – Listen for mouse movement triggers test by Logan (@logan70) on CodePen.
The callbacks here just print the string, and if the callbacks were more complex, you’d expect a lot of stress on the browser, potentially reducing the user experience.
Listener callbacks for events such as resize, Scroll, or mousemove are frequently fired, so we want to limit them.
Second, the realization of ideas
Function throttling simply means that continuous function calls are executed only once at intervals. There are two preliminary implementation ideas:
1. Use the time stamp
Set a comparison timestamp. When the event is triggered, use the current timestamp minus the comparison timestamp. If the difference is greater than the specified interval, execute the function and replace the comparison timestamp with the current timestamp. If the difference is less than the set interval, the function is not executed.
function throttle(method, wait) {
// If the timestamp is initialized to 0, the execution is triggered immediately. If the timestamp is initialized to the current time, the execution will be triggered milliseconds later
let previous = 0
return function(. args) {
let context = this
let now = new Date().getTime()
// If the interval is longer than wait, execute method and update the comparison timestamp
if (now - previous > wait) {
method.apply(context, args)
previous = now
}
}
}
Copy the code
Check out the online example: Function Throttling – Initial implementation timestamp by Logan (@logan70) on CodePen.
2. Use a timer
When the event is triggered for the first time, the timer is set, and the function is executed after wait milliseconds and the timer is set to NULL. When the event is triggered later, the timer is not executed if it exists, and the timer is set again if it does not exist.
function throttle(method, wait) {
let timeout
return function(. args) {
let context = this
if(! timeout) { timeout = setTimeout((a)= > {
timeout = null
method.apply(context, args)
}, wait)
}
}
}
Copy the code
Check out the online example: Function Throttling – Initial implementation of timer by Logan (@logan70) on CodePen.
3. Comparison of the two methods
- First trigger: immediately executed with a timestamp implementation (if previous is set to 0); Use the timer implementation will set the timer, wait milliseconds after the execution.
- Stop triggering: when using the timestamp implementation, stop triggering will not be executed again; When a timer is used, it is executed once after the trigger is stopped because a timer exists.
3. Throttling application scenarios
- DOM element drag-and-drop implementation (
mousemove
) - Shooting game
mousedown/keydown
Event (can only fire 1 bullet per unit time) - Calculate the distance the mouse moves (
mousemove
) - Canvas simulation Board function (
mousemove
) - Search lenovo (
keyup
) - Listen for scroll events to determine whether to automatically load more to the bottom of the page: Here
scroll
addeddebounce
The bottom of the page will only be determined after the user stops scrolling; If it isthrottle
If the page is scrolling, it will be judged once in a while
Fourth, function throttling final version
Code speaking, there are errors kindly pointed out
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result Records the return value of method
let timeout, result
// Record the last time the function was executed (not every update)
let methodPrevious = 0
// Record the last time the callback was triggered (updated each time)
let throttledPrevious = 0
let throttled = function(. args) {
let context = this
// With Promise, you can get the return value of the original function when the callback is fired
return new Promise(resolve= > {
let now = new Date().getTime()
// The interval between two adjacent triggers
let interval = now - throttledPrevious
// Update the trigger time for the next time
throttledPrevious = now
// Set methodPrevious to now, remaining = wait > 0, and pretend that methodPrevious has just been executed
// Leading = false
// Add one of the following conditions
// 1. First trigger (methodPrevious is 0)
// 2. When trailing is true, the stop trigger time exceeds wait, the function in the timer executes (methodPrevious is set to 0), and then fires again
// 3. When trailing is set to false (methodPrevious will not be set to 0 if no timer is set), stop triggering after wait (interval > wait)
if (leading === false&& (! methodPrevious || interval > wait)) { methodPrevious = now// To be safe, set the clear timer to NULL
// Pretend to have just performed the full XD you want to pretend
if (timeout) {
clearTimeout(timeout)
timeout = null}}// The interval between the next execution of the original function
let remaining = wait - (now - methodPrevious)
// 1. When leading is true, execute immediately on the first trigger
// 2. Reach the next time to execute the original function
// 3. The system time is modified
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// Update the comparison timestamp, execute the function and record the return value, and pass it to resolve
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// Dereferencing to prevent memory leaks
if(! timeout) context = args =null
} else if(! timeout && trailing ! = =false) {
timeout = setTimeout((a)= > {
// Set methodPrevious to 0 when leading is false
// If you don't set methodPrevious to 0, the callback doesn't fire for a long time after the timer fired
// Remaining is negative when triggered next time, and the original function is executed immediately, which violates the setting of Leading being false
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// Dereferencing to prevent memory leaks
if(! timeout) context = args =null
}, remaining)
}
})
}
// Add the cancellation function, which can be used as follows
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
Copy the code
The enclosing function of the throttled function is also required to wait for the result of the execution using Async/Await syntax
See the code for usage:
function square(num) {
return Math.pow(num, 2)}// let throttledFn = throttle(square, 1000)
// let throttledFn = throttle(square, 1000, {leading: false})
// let throttledFn = throttle(square, 1000, {trailing: false})
let throttledFn = throttle(square, 1000, {leading: false.trailing: false})
window.onmousemove = async() = > {try {
let val = await throttledFn(4)
// When the function is not executed, val is undefined
if (typeofval ! = ='undefined') {
console.log(The function returns a value of${val}`)}}catch (err) {
console.error(err)
}
}
// When the mouse moves, output every 1S interval:
// The return value of this function is 16
Copy the code
Check out the online example: Function Throttling – final by Logan (@logan70) on CodePen.
See the detailed implementation steps below
Five, the function Throttle Throttle specific implementation steps
1. Optimize the first version: merge the two implementations
The effect of this is that the initial trigger is immediate, and the trigger is stopped and executed again
function throttle(method, wait) {
let timeout
let previous = 0
return function(. args) {
let context = this
let now = new Date().getTime()
// The amount of time left before the next function execution
let remaining = wait - (now - previous)
// If there is no remaining time or the system time has been modified
if (remaining <= 0 || remaining > wait) {
// Clear and set to NULL if the timer still exists
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// Update the comparison timestamp and execute the function
previous = now
method.apply(context, args)
} else if(! timeout) {// If there is time left but the timer does not exist, set the timer
// Remaining Performs the function and updates the comparison timestamp after milliseconds
// Set the timer to NULL
timeout = setTimeout((a)= > {
previous = new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Copy the code
Let’s get this straight. Suppose a callback is triggered continuously:
- First trigger: the comparison timestamp is 0, the remaining time is negative, the function is executed immediately and the comparison timestamp is updated
- Second trigger: The remaining time is positive and the timer does not exist. Set the timer
- After the trigger: the remaining time is positive, the timer exists, and no other action is performed
- Until the remaining time is less than or equal to 0 or the function is executed within the timer (since the callback is triggered at an interval and setTimeout is incorrect, which is triggered first is uncertain).
- If the function in the timer is executed, the timer is updated and set to NULL. The next time the function is triggered, the timer continues to be set
- If no function is executed in the timer but the remaining time is less than or equal to 0, the clear timer is set to NULL, the function is executed immediately, the time stamp is updated, and the setting of the timer continues at the next trigger
- After firing: If firing is not stopped at the two special time points described above, there will be a timer and the function will be executed once more
Check out the online example: Function Throttling – Optimization first edition: Merge two implementations by Logan (@logan70) on CodePen.
2. Optimized version 2: Provides the configuration item to be executed immediately upon the first trigger
// Leading is the configuration item that controls whether the function is executed immediately upon first firing
function throttle(method, wait, leading = true) {
let timeout
let previous = 0
return function(. args) {
let context = this
let now = new Date().getTime()
/ /! Previous indicates the first firing or the first firing after the timer is fired. Update previous to NOW if it does not need to be executed immediately
// In this case, remaining = Wait > 0, the operation is not executed immediately, but the timer is set
if(! previous && leading ===false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if(! timeout) { timeout = setTimeout((a)= > {
// If leading is false, set previous to 0,
// The next trigger will be synchronized with the now on the next trigger, and the first trigger (for the user) will not be executed immediately
// If you set this parameter to the current timestamp and stop triggering for a period of time, the remaining value of the next triggering is a negative value, and the operation is executed immediately
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Copy the code
Check out the online example: Function Throttling – Optimised version 2: Provides a configuration item for immediate execution on the first trigger by Logan (@logan70) on CodePen.
3. Optimized version 3: Provides the configuration item of whether to execute once after triggering is stopped
// Trailing is the configuration item that controls whether the stop trigger is followed once
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
return function(. args) {
let context = this
let now = new Date().getTime()
if(! previous && leading ===false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if(! timeout && trailing ! = =false) {
// If there is time left but the timer does not exist and the trailing value is not false, set the timer
Trailing with false is equivalent to using only the timestamp for throttling
timeout = setTimeout((a)= > {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Copy the code
Check out the online example: Function Throttling – Optimization version 3: Provides a configuration item for whether to execute once the stop is triggered by Logan (@logan70) on CodePen.
4. Optimized version 4: Provide cancellation function
Sometimes we need to be able to manually cancel throttling during the non-triggering period. This code is implemented as follows:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
// Assign the returned anonymous function to the throttLED to add a cancellation method to it
let throttled = function(. args) {
let context = this
let now = new Date().getTime()
if(! previous && leading ===false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if(! timeout && trailing ! = =false) {
timeout = setTimeout((a)= > {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
// Add the cancellation function, which can be used as follows
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
// Return the function after throttling
return throttled
}
Copy the code
Check out the online example: Function Throttling – Optimization version 4: Provides cancellation by Logan (@logan70) on CodePen.
5. Optimize version 5: Handle return values from original functions
The function that requires a throttling may have a return value, and we will handle this case by returning the return value of the function in the return function of debmentioning, but this is actually problematic. If the original function is executed within setTimeout, the return value cannot be obtained synchronously, so we use the Promise to handle the return value.
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result Records the execution result of the original function
let timeout, result
let previous = 0
let throttled = function(. args) {
let context = this
Return a Promise so that you can get the return value of the original function using then or Async/Await syntax
return new Promise(resolve= > {
let now = new Date().getTime()
if(! previous && leading ===false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = method.apply(context, args)
// Pass the return value of function execution to resolve
resolve(result)
} else if(! timeout && trailing ! = =false) {
timeout = setTimeout((a)= > {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
// Pass the return value of function execution to resolve
resolve(result)
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
Copy the code
Use method 1: Use then to get the return value of the original function when calling the throttled function
function square(num) {
return Math.pow(num, 2)}let throttledFn = throttle(square, 1000.false)
window.onmousemove = (a)= > {
throttledFn(4).then(val= > {
console.log(The return value of the function is:${val}`)})}// When the mouse moves, output after every 1S interval:
// The return value of this function is 16
Copy the code
Use method 2: The enclosing function of the throttled function is called to wait for the result of the execution using Async/Await syntax
See the code for usage:
function square(num) {
return Math.pow(num, 2)}let throttledFn = throttle(square, 1000)
window.onmousemove = async() = > {try {
let val = await throttledFn(4)
// When the function is not executed, val is undefined
if (typeofval ! = ='undefined') {
console.log(The function returns a value of${val}`)}}catch (err) {
console.error(err)
}
}
// When the mouse moves, output every 1S interval:
// The return value of this function is 16
Copy the code
Check out the online example: Function Throttling – Optimization 5th edition: Handling the return value of the original function by Logan (@logan70) on CodePen.
6. Optimized version 6: You can disable both immediate execution and post-execution
One drawback to function throttling that mimes the underscore implementation is that leading: false and Trailing: false cannot be set at the same time.
If you set the value of trailing to false when you move the mouse out, for example, the timer will not be set when you stop the trigger. So if you move the mouse in again after the set time, the remaining will be negative, and the cursor will execute immediately. Leading is violated: False, here we optimize the idea as follows:
Calculate the interval between two consecutive callbacks. If the interval is greater than the set value, reset the comparison timestamp to the current timestamp, which is equivalent to the first trigger, to prevent the first (false) immediate execution of the effect, the code is as follows:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout, result
let methodPrevious = 0
// Record the last time the callback was triggered (updated each time)
let throttledPrevious = 0
let throttled = function(. args) {
let context = this
return new Promise(resolve= > {
let now = new Date().getTime()
// The interval between two triggers
let interval = now - throttledPrevious
// Update the trigger time for the next time
throttledPrevious = now
// Change the condition to reset methodPrevious when the interval is greater than wait and leading is false, so that immediate execution is not allowed
if (leading === false&& (! methodPrevious || interval > wait)) { methodPrevious = now }let remaining = wait - (now - methodPrevious)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// Dereferencing to prevent memory leaks
if(! timeout) context = args =null
} else if(! timeout && trailing ! = =false) {
timeout = setTimeout((a)= > {
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// Dereferencing to prevent memory leaks
if(! timeout) context = args =null
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
methodPrevious = 0
timeout = null
}
return throttled
}
Copy the code
Check out the online example: Function Throttling – Optimization version 6: You can disable both immediate execution and post-execution by Logan (@logan70) on CodePen.
6. Reference articles
Learn to save money with underscore
The underscore function implements throttling
If there are any mistakes or irregularities, please be sure to correct them. Thank you very much.