preface

In daily development or interview, anti-shake and throttling should be a point of frequent occurrence. This article was mainly based on two articles of Hu Yuba (hereafter referred to as ohami) on flutter prevention and throttling. Because WHEN I read his article, I also had some confusion about the code, and there were some stuck places, so I wanted to throw out all the problems I met and understand step by step. In this paper, the specific scene demo uses his example, not the scene example alone.

Definition of anti – shake and throttling

  • Buffeting: The event continues to fire, but the function is executed only n seconds after the event has stopped firing.
  • Throttling: The function is executed every n seconds while the event continues to fire.

Image stabilization

An event that continues to fire does not execute until n seconds after the event stops firing before the function is executed.

// first edition const debounce =function(func, delay) {
    let timeout;
    return function () {
        const context = this;
        const args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(() => { func.apply(context, args) }, delay); }}Copy the code

The first version doesn’t have any difficulties. The timer is cleared as the user continues to trigger, and when he triggers the last time, a timer is generated and the method in the timer executes at delay seconds.

New requirement: wait until the event stops firing and want to execute the function immediately. Then wait until n seconds after the trigger has stopped, and then restart the execution.

First, break down the requirements:

  • Execute function immediately
  • The trigger is stopped for n seconds and then triggered again

Func.apply (context, args) is easy to implement by executing functions immediately. But it’s not possible to call func all the time while the user is firing, so you need a field to determine when the func function can be executed.

// second version const debounce =function (func, delay) {
    let timer,
        callNow = true; // Whether to execute the function immediatelyreturn function () {
        const context = this;
        const args = arguments;
        if (timer) clearTimeout(timer);
        if(callNow) { func.apply(context, args); // Trigger the event to execute callNow = immediatelyfalse; // Set the identity tofalseTo ensure that subsequent events triggered within delay seconds cannot execute the function. }else {
            timer = setTimeout(() => {
                callNow = true; // The function can be triggered again after delay seconds. }, delay) } } }Copy the code

New requirement: Add an immediate parameter to determine whether to execute it immediately.

In fact, through the simplified version of the above, this time add a parameter field to distinguish it is very good to achieve.

const debounce2 = function (func, delay, immediate = false) {
    let timer,
        callNow = true;
    return function () {
        const context = this;
        const args = arguments;
        if (timer) clearTimeout(timer);
        if (immediate) {
            if(callNow) func.apply(context, args); // Trigger the event to execute callNow = immediatelyfalse;
            timer = setTimeout(() => {
                callNow = true; // It will be n seconds before the function is triggered again. }, delay) }else {
            timer = setTimeout(() => {
                func.apply(context, args);
            }, delay)
        }
    }
}
Copy the code

The return value

The getUserAction function may have a return value, so you need to return the result of the function here as well. When immediate is false, the value will always be undefined because of setTimeout. So return the result of the function only if immediate is true.

const getUserAction = function(e) {
    this.innerHTML = count++;
    return 'Function Value';
}

const debounce = function (func, delay, immediate = false) {
    let timer,
        result,
        callNow = true;
    return function () {
        const context = this;
        const args = arguments;
        if (timer) clearTimeout(timer);
        if (immediate) {
            if(callNow) result = func.apply(context, args);
            callNow = false;
            timer = setTimeout(() => {
                callNow = true; // It will be n seconds before the function is triggered again. }, delay) }else {
            timer = setTimeout(() => {
                func.apply(context, args);
            }, delay)
        }
        return result;
    }
}
    
// demo test    
const setUseAction = debounce(getUserAction, 2000, true); // Display the function return value box. AddEventListener ('mousemove'.function (e) {
        const result = setUseAction.call(this, e);
        console.log('result', result);
    })
Copy the code

cancel

We want to be able to cancel debounce, which allows the user to cancel the buffering after performing this method (cancel) and then execute it again when the user triggers it again.

Need to consider: Cancel the anti – shake, in fact, to clear the existing timer. This allows the function to execute immediately when the user triggers it again. Hey hey 😝 is very simple ah!

const debounce = function (func, delay, immediate = false) {
    let timer,
        result,
        callNow = true;
    const debounced = function () {
        const context = this;
        const args = arguments;
        if (timer) clearTimeout(timer);
        if (immediate) {
            if(callNow) result = func.apply(context, args);
            callNow = false;
            timer = setTimeout(() => {
                callNow = true; // It will be n seconds before the function is triggered again. }, delay) }else {
            timer = setTimeout(() => {
                func.apply(context, args);
            }, delay)
        }
        return result;
    };
    debounced.cancel = function(){ clearTimeout(timer); timer = null; }}Copy the code

After such a series of split is not immediately feel anti – shake is so the thing, it is not difficult ~

The throttle

There are two main implementations of throttling: 1. Timestamp; 2. Set the timer.

The time stamp

When the event is triggered, the current timestamp is retrieved and the previous timestamp is subtracted (initially set to 0). If the period is longer than the set period, the function is executed and the timestamp is updated to the current timestamp. If the value is smaller than the value, the value is not executed.

const throttle = function(func, delay) {
    letprev = 0; // Set the initial timestamp to 0 to ensure that the function executes on the first triggerreturn function(){
        const context = this;
        const args = arguments;
        const now = +new Date();
        if(now - prev > delay) { func.apply(context, args); prev = now; }}}Copy the code

Existing problems

The function is executed every delay second. If the last trigger time is less than delay, the now-prev < delay causes that the last trigger does not execute the function.

The timer

When an event is triggered, a timer is set. When the event is triggered again, the timer will not be executed if it exists. Wait until the internal timer method runs out, then empty the timer and set the next timer.

const throttle = function(func, delay){
    let timer;
    return function(){
        const context = this;
        const args = arguments;
        if(! timer) { timer =setTimeout(() => { timer = null; // delay second Resets the timer value to null in order to reset a new timer. func.apply(context, args); }, delay); }}}Copy the code

Existing problems

The function is not executed when the event is first raised.

Shuangjian combination

This release addresses two requirements:

  • The event that is triggered for the first time executes immediately
  • The event is executed again after the event is stopped

I’ll post his code here.

To be honest, I was confused when I first saw this code, and it took me a while to fully figure it out. Here, I’ll write down my thoughts on how to understand this code to help you implement this requirement layer by layer.

Let’s look at the second requirement (executing the event again after the event is stopped), which is essentially delaying the event, so I’ll use setTimeout first. But there is a question of how many seconds before the second parameter of setTimeout fires. Suppose I execute the function every 3s, three times, and I stop firing at 9.5. How many seconds will it take to execute the final trigger event? (12-9.5 = 2.5s)

// Pseudo snippet like this const throttle1 =function(func, delay){
    let timer,
        prev = 0;
    return function(){ const context = this; const args = arguments; const now = +new Date(); const remaining = delay - (now - prev); // Key point: remaining time // set! The timer condition is used to prevent an event from being triggered to generate a new timer if a timer already exists.if(remaining > 0 && ! timer) { timer =setTimeout(() => {
                prev = +new Date();
                timer = null;
                func.apply(context, args);    
            }, remaining)
        }
    }
}
Copy the code

Looking at the first requirement (the first time the event is triggered immediately), setting prev to 0 for the first time ensures that delay – (now-prev) must be less than 0 for the first time.

// Pseudo snippet like this const throttle2 =function(func, delay){
    let timer,
        prev = 0;
    return function(){ const context = this; const args = arguments; const now = +new Date(); const remaining = delay - (now - prev); // Key point: next trigger func remaining time // set! The timer condition is designed to trigger the event again to create a timer if there is already one.if(Remaining <= 0) {// What does this code actually mean?if(timer) { clearTimeout(timer); timer = null; } prev = now; func.apply(context, args); }}}Copy the code

Full version

const throttle = function(func, delay) {
    let timer,
        prev = 0;
    return function(){
        const context = this;
        const args = arguments;
        const now = +new Date();
        const remaining = delay - (now - prev);
        if (remaining <= 0) {
            prev = now;
            func.apply(context, args);    
        } else if(! timer) { timer =setTimeout(() => {
                prev = +new Date();
                timer = null;
                func.apply(context, args);    
            }, remaining)    
        }
    }
}
Copy the code

Now simulate the operation based on the above two codes (assuming the delay value is 3) :

  • First trigger:remainingIf the value is less than 0, the command is executed directlyfuncSimultaneous update of functionsprevThe value of (prev = now).
  • Trigger after 1s:remainingAnd a value of 2timerA value ofundefined. A timer is set (executed after 2s) and the code in the timer is executed after 2s (updated)prevValue; performfuncFunctions; resettimerThe value).
  • Trigger after 2s:remainingAnd the value is 1timerIf it has a value, it doesn’t walk into any branches, that is, nothing happens.
  • Trigger after 3s:remainingThe value is 0 andtimerThe value is null and updates at this timeprevThe value of the willtimerSet to null and executefuncFunction.
  • Trigger after 4s:remainingAnd the value is 1timerIf the value is null, the above will be repeatedIt will trigger after 1sGenerate a new timer. The code in the timer will execute after 2s.
  • Trigger after 9.2s (stop trigger can be executed again) :remainingAnd a value of 2.8timerThe value is null, a new timer is generated, and the code in the timer is executed after 2.8s.

I don’t know if you will have such a question, I stopped triggering at 9.2 seconds, and then I triggered again at 10 seconds. Will that generate more new timers? In fact, this operation is similar to steps 2 and 3 above. When 10s fires again, the value of remaining is 2, but the timer has a value, so it does not enter any branches, and nothing happens.

I do not know after this split explanation, the audience master has the above screenshots of the code a little clearer 😊?

Optimized version

Sometimes you want to have no head and no tail or a tail and no head. Set options as the third parameter, and then determine the desired effect based on the passed value. Leading :false: disables the first execution. Trailing :false disables the stop-triggered callback.

As usual, take a look at his code. When I first looked at this version of the code, I had the following questions:

  • whylaterI’m not going to write it directlyprevious = new Date().getTime()And writtenprevious =options.leading === false ? 0 : new Date().getTime()? ;
  • Why should there beif (! timeout) context = args = nullWhat about this code?
  • What does this code mean? Could it possibly go this far?
if (timeout) {
    clearTimeout(timeout);
    timeout = null;
}
Copy the code

Set leading = false to disable the first execution. The key to remaining is that the value of remaining is less than 0. In other words, the value of remaining is greater than 0. (Set the initial value of prev to the value of now.)

const throttle = function(func, delay, option = {}) {
    let timer,
        prev = 0;
    return function(){ const context = this; const args = arguments; const now = +new Date(); // Set the prev value equal to the now value for the first timeif(! prev && option.leading ===false) { prev = now; } Const remaining = delay - (now-prev);} Const remaining = delay - (now-prev);if (remaining <= 0) {
            prev = now;
            func.apply(context, args);    
        } else if(! timer) { timer =setTimeout(() => {
                prev = option.leading === false? 0 : +new Date(); // The reason for this is explained below. timer = null; func.apply(context, args); }, remaining) } } }Copy the code

See also how trailing = false disables the stop-triggered callback. Also think about what causes it to stop firing and then execute again? In fact, the value of remaining is greater than 0, and when it is greater than 0, a timer is generated, causing the function to execute after remaining seconds even if the firing is stopped. So just add option.trailing! To the condition that generates the timer code. == false disallows the aborted callback.

const throttle = function(func, delay, option = {}) {
    let timer,
        prev = 0;
    return function(){
        const context = this;
        const args = arguments;
        const now = +new Date();
        if(! prev && option.leading ===false) {
            prev = now;
        }
        const remaining = delay - (now - prev);
        if(remaining <= 0) { prev = now; func.apply(context, args); // When option.trailing is set tofalse, you can never walk into this branch, and there will be no timer. }else if(! timer && option.trailing ! = =false) {
            timer = setTimeout(() => {
                prev = option.leading === false ? 0 : +new Date();
                timer = null;
                func.apply(context, args);    
            }, remaining)    
        }
    }
}
Copy the code

Explanation question 1

Prev = option.leading === false 0: +new Date() instead of prev = +new Date(). The key point is that when prev = 0, the event must be executed if(! Pre && option.leading === false) prev = now, which ensures that the value of Remaining is always greater than 0, that is, whenever the user triggers the event again, the code will go to the else if branch. As an example, (delay is 3s) ~

  • The first time the user triggers a slide event,remainingValue greater than 0, so a timer is generated and the timer internal code executes after 3 seconds.
  • At this point, it is assumed that the user does not trigger the event for 3s, but leaves the slippable area at 2s. After 1s, the corresponding function in the timer will still execute as usual. At this point, the watershed comes outprev = +new Date()And assume that the user fires the event again 10 seconds later because nowprevHave a value, anddeay - (now - prev)Less than 0 (because now the value of the now-prev is 10, greater than 3), so it goesif(remaining <= 0)Branch, which is executed immediatelyfuncFunction. This does not meet the requirements of the first trigger (Note that the first trigger is not just the first trigger. If you leave the trigger area and try to trigger it later, it will still be considered the first trigger. This point must be understoodThe function is not executed.
  • Take a look atprev = option.leading === false ? 0 : +new Date(), after 10 sprevIf the user triggers the event again, the event will be executedprev = nowThis code, so at this point can make sureremainingIs greater than 0, which ensures that the function will not be executed the first time the user fires the event again. Instead, a timer is generated and the method in the timer is executed after 3s.

Explanation Question 2

Context = args = null is used primarily to free up memory, because JavaScript has automatic garbage collection that finds values that are no longer in use and frees up memory. The garbage collector performs a release operation at regular intervals.

Explanation Question 3

I’m not really sure about that yet. I guess this is in case the timer code timeout = NULL is not executed immediately after the specified time (timeout still has a value). I feel that this code handles such extreme cases and ensures that the timeout value will be set to NULL.

conclusion

This is my understanding of anti – shake and throttling. Next up is an article on anti-vibration and throttling. Hope everyone can discuss together in the comments section, any good idea can also throw out 😬~

Refer to the article

  • Underscore for JavaScript topics
  • JavaScript topics follow the science of underscore