directory

  • concept
  • implementation
  • reference
    • Debounce the underscore of the article
    • Debounce lodash article
  • perfect
  • Question and answer
  • conclusion

concept

There is no standard definition of anti-shake, so it can only be assumed…

If the same event is triggered multiple times within a period of time, the event that is triggered only once is executed.

What does that mean? First look at the crowded subway and bus scene in daily life.

In the crowded subway, bus, everyone began to get on the bus one by one, the driver will wait for the last one to get on the bus before closing the door and starting the bus (in the whole process, no matter how many babies, the driver only see whether the last baby has got on the bus, trigger the door) to the kindergarten direction.

Here we break down the scene as follows:

  • Event: Close and leave
  • When trigger event: Trigger event every 5 seconds after the last baby

Note: If baby No. 1 gets on the bus and the door closes for 5 seconds, baby No. 2 and baby No. 3 get on the bus and the door closes for 5 seconds after baby No. 3 gets on the bus and the door closes for 5 seconds after the last baby gets on the bus.

What are the scenarios that need to be used during development?

Resize event for the browser window

The browser scroll bar scroll event

Important things need to be repeated three times:

Remember: Do anti-shake only once!

Remember: Do anti-shake only once!

Remember: Do anti-shake only once!

Based on some of the above, it should be known that anti-shake is used to address performance optimization for triggering the same event with high frequency in a certain period of time, so there is no specific scenario that is entirely dependent on business requirements.

implementation

After introducing the concepts, let’s look at how to achieve anti-shake.

Let’s comb through a list of steps for the browser window resize event scenario:

  1. Implement event handlers.

  2. Use setTimeout to create a timer that triggers the handler.

  3. Before creating a timer, cancel the last timer.

<html>
  <head>
    <title>resize</title>
    <script type="text/javascript">
      // There is no optimization in normal operation, dragging and zooming the browser window will constantly trigger the event handler.
      function resize() {
        console.log('Triggers the resize event for the zoom browser window.');
      }
      window.onresize = resize;
    </script>
  </head>
  <body>
    <p>Changing the size of the browser window triggers the resize event.</p>
  </body>
</html>
Copy the code

Implement event handlers.

<html>
  <head>
    <title>resize</title>
    <script type="text/javascript">
      function resize() {
        console.log('Triggers the resize event for the zoom browser window.');
      }
      /** ** anti-shake function *@param {Function} Fn executes the function *@param {Number} Wait time *@returns Function Executable Function */
      function debounce(fn, wait) {
        return fn;
      }
      Onresize receives an executable function. Because an executable is called each time the resize event is triggered, the return value of the debounce() function is an executable.
      window.onresize = debounce(resize, 3000);
    </script>
  </head>
  <body>
    <p>Changing the size of the browser window triggers the resize event.</p>
  </body>
</html>
Copy the code

This is just an overview of the steps. Next, we will use setTimeout to create the timer and trigger the handler when the time is up.

<html>
  <head>
    <title>resize</title>
    <script type="text/javascript">
      function resize() {
        console.log('Triggers the resize event for the zoom browser window.');
      }
      /** ** anti-shake function *@param {Function} Fn executes the function *@param {Number} Wait time *@returns Function Executable Function */
      function debounce(fn, wait) {
        // The timer is not added, so the event handler is triggered frequently.
        // If (fn) {// if (fn) {// if (fn) {// if (fn) {// if (fn) { You do it.
        return function () {
          console.log('---- anonymous function --'); // Prints to the console at a high frequency
          setTimeout(fn, wait); // Use the timer to execute the fn function to the time (reduce the trigger frequency)
        };
      }
      Onresize receives an executable function. Because an executable is called each time the resize event is triggered, the return value of the debounce() function is an executable.
      window.onresize = debounce(resize, 3000);
    </script>
  </head>
  <body>
    <p>Changing the size of the browser window triggers the resize event.</p>
  </body>
</html>
Copy the code

If the above code is executed it should look similar to the GIF below. Compare the output frequency of the —- anonymous function to see that the output of the function executed using the timer triggers the resize event of the browser window. The frequency of…

It is recalled that the anti-shake concept triggers the same event several times within a certain period and only executes the event that is triggered once. Although the frequency is reduced, there is still a third step to execute the event that is triggered only once mentioned in the concept — cancel the last timer before creating the timer each time.

<html>
  <head>
    <title>resize</title>
    <script type="text/javascript">
      function resize() {
        console.log('Triggers the resize event for the zoom browser window.');
      }
      /** ** anti-shake function *@param {Function} Fn executes the function *@param {Number} Wait time *@returns Function Executable Function */
      function debounce(fn, wait) {
        var timerId = null;
        // The timer is not added, so the event handler is triggered frequently.
        // If (fn) {// if (fn) {// if (fn) {// if (fn) {// if (fn) { You do it.
        return function () {
          console.log('---- anonymous function --'); // Prints to the console at a high frequency
          if(timerId ! = =null) {
            clearTimeout(timerId); // Cancel the created timer
          }
          timerId = setTimeout(fn, wait); // Set the timer
        };
      }
      Onresize receives an executable function. Because an executable is called each time the resize event is triggered, the return value of the debounce() function is an executable.
      window.onresize = debounce(resize, 3000);
    </script>
  </head>
  <body>
    <p>Changing the size of the browser window triggers the resize event.</p>
  </body>
</html>
Copy the code

Following the three steps above, the timer function created when the browser window is constantly zooming is not executed as infrequently as before. Instead, the zooming event is executed 3000ms(3 seconds) after the browser window stops zooming, which is why the third step is needed.

reference

Even though underscore and LoDash implement debounce, they are much worse than underscore and LoDash implementations… So let’s first look at how these two libraries implement Debounce and work on it.

Debounce the underscore of the article

Source code based on v1.10.2 version, directly in the source code to add understanding notes.

/** ** underscore. Debounce@param {Function } Event handler *@param {Number} Wait trigger time *@param {Boolean} Immediate Whether to execute the event handler * immediately@returns Function Executable Function */
function debounce(func, wait, immediate) {
  //timeout Indicates whether the timer has been created
  //result records the return value of the func function.
  var timeout, result;

  var later = function (context, args) {
    timeout = null;
    if (args) result = func.apply(context, args);
  };

  // See ES6 rest before you know restArguments function parameter syntax, refer to https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Rest_p arameters
  // restArguments is a function provided by underscore to implement ES6-like REST parameter syntax, which returns an executable function
  // The restArguments function is called here just to do something with the parameters (not the point), or you can think of it as an anonymous function.
  // var debounced=function(){}
  var debounced = restArguments(function (args) {
    // If the timer exists, cancel it (so that no matter how many times the timer is triggered, the previous cancel will be restarted, to ensure that only one timer is triggered).
    if (timeout) clearTimeout(timeout);
    // According to the concept of anti-shake mentioned above, anti-shake is executed only once. With crowded bus and subway scenarios, it can be found that only 'this time' is usually executed when the event is triggered for the last time, but there are indeed business scenarios where the event is executed when the event is triggered for the first time. Therefore, the immediate parameter is used to determine the following two situations
    //true:1. Execute the event handler immediately when the event is triggered for the first time;
    //false:2. Execute the event handler for the last time;
    // In either case it is the same as "Anti-shake only once!"
    If () {if () {if () {if () {if ();
    if (immediate) {
      Immediate =true immediate=true
      // First check whether the timer exists
      varcallNow = ! timeout;// Underline - underline - underline
      If (args) is always false. If (args) is false. If (args) is false. Context (args) : context (args); context (args) : context (args); context (args) : context (args); context (args) : context (args); This, in conjunction with the following if(callNow), does all subsequent cancellations after the first trigger.
      timeout = setTimeout(later, wait);
      // If (timer does not exist), instead of using setTimeout to set the timer, execute the func function immediately, so that the first event is executed immediately
      if (callNow) result = func.apply(this, args);
    } else {
      // Delay, as a tool function for underscore, does two things
      //1. Call restArguments to fix the parameters
      / / 2. Return to setTimeout
      Timeout =setTimeout(later,wait); It's just that you can't pass parameters to Later, so the delay function internally passes this and args parameters to later via restArguments
      timeout = delay(later, wait, this, args);
    }
    // If immediate=true, the result value is returned. Immediate =false; result is undefined
    If (immediate) result = func. Apply (this, args); if(immediate) result = func. The else function is actually a setTimeout timer. Since the code is executed asynchronously, the code has already executed a return result before the trigger time. So result=undefined.
    return result;
  });
  // Add static method to cancel timer
  debounced.cancel = function () {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
}
Copy the code

The understanding of the source code needs a bit more taste… Therefore, the above understanding is summarized as follows:

  • Implementing event Handler (DebMentioning)

  • Implement immediate execution, last execution (immediate)

  • Processing parameters

  • Processing return value

  • Implement cancel timer (debmentioning. Cancel)

Debounce lodash article

Source code based on v4.17.15 version, directly in the source code to add understanding notes.

/** ** lodash. Debounce function understand *@param {Function } Event handler *@param {Number} Wait trigger time *@param {Object} Options Configures *@returns Function Executable Function */
function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait, // Maximum wait time (interval time), this parameter is actually used as a throttling
    result,
    timerId,
    lastCallTime, // The last time the event was triggered (in milliseconds)
    lastInvokeTime = 0.// The last time the event handler was executed (milliseconds)
    leading = false.// The first time an event is raised, the event handler is executed immediately
    maxing = false.// Enable or not, according to this parameter is to determine whether to start throttling mode
    trailing = true; // Wait for time to execute the event handler after the event is triggered

  if (typeoffunc ! ='function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  wait = toNumber(wait) || 0; // Wait time
  if(isObject(options)) { leading = !! options.leading;// Convert to Boolean type
    maxing = 'maxWait' in options; // If the maximum wait time is configured to achieve the effect of throttling, intermittent trigger events according to the maximum interval
    maxWait = maxing
      ? nativeMax(toNumber(options.maxWait) || 0, wait)
      : maxWait; // Set the maximum interval
    trailing = 'trailing' inoptions ? !!!!! options.trailing : trailing; }/** ** Immediately executes (calls) the event handler (func) *@param {Number} Time Call time (milliseconds) *@returns* /
  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;

    lastArgs = lastThis = undefined; / / reset
    lastInvokeTime = time; // Reset the last time the event handler was executed = this time (milliseconds)
    result = func.apply(thisArg, args); / / execution func
    return result;
  }
  This function is only responsible for determining whether the invokeFunc function needs to be called for the first time the event is triggered (execute the event handler immediately) * but it also creates a timer, why do two things (1). Determine whether to execute the event function immediately; 2. Create a timer. * is its duty to do the first thing should do, the second thing to create a timer is paving the way for the subsequent operations * don't forget to stabilization function debounce the third parameter is available with {leading: true, trailing: true} both parameter to true is * The trailing:true event is executed immediately after the trailing:true event. The trailing:true event is executed immediately after the trailing:true event Why are all allowed true? See question 1 * at the end of the article@param {Number} Time Call time (milliseconds) *@returns* /
  function leadingEdge(time) {
    lastInvokeTime = time; // Reset the last time the event handler was executed = this time (milliseconds)
    timerId = setTimeout(timerExpired, wait); // Create a timer
    // Determine whether invokeFunc needs to be called based on the leading option
    return leading ? invokeFunc(time) : result;
  }
  /** ** calculates the time when setTimout is triggered *@param {*} time
   * @returns* /
  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }
  /** ** Calculates whether the call * should be initiated based on the time difference@param {Number} time
   * @returns Boolean* /
  function shouldInvoke(time) {
    // Time difference between two anti-shake = time when anti-shake is triggered this time - time when anti-shake was triggered last time
    //(used in throttling mode) event handler time difference = time when the current event handler is triggered - time when the event handler was last executed
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;

    // If one of the items returns true, the call will be made
    //1. LastCallTime === undefined if the value is undefined, it is the first time (because lastCallTime is initialized to undefined)
    TimeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall >= timeSinceLastCall
    // If the timer will be executed after 5 seconds, when the debmentioning event will be triggered continuously during the 5 seconds period, the time difference of triggering the anti-knock event will be calculated.
    If the time difference is less than the set wait time of 5 seconds, false is returned for events that occurred within 5 seconds, and true is returned for events that did not occur within 1-5 seconds if the time difference is greater than 5 seconds
    TimeSinceLastCall < 0 timeSinceLastCall < 0 timeSinceLastCall < 0 If you manually adjust the system time to a certain day, is the time difference less than 0 (past time - present time <0)?
    //4. Maxing && timeSinceLastInvoke >= maxWait Maxing = true
    // timeSinceLastInvoke >= maxWait Determines whether the time difference of the event handler is greater than the maximum interval event set.

    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }
  /** ** setTimeout(timerExpired, wait) * continuously recalcitates the remaining time *@returns* /
  function timerExpired() {
    var time = now();

    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // recalculate the time
    timerId = setTimeout(timerExpired, remainingWait(time));
  }
  /** ** Determine if the invokeFunc function needs to be called after the trigger event (execute the event handler immediately) * default trailing:true uses the execute the event handler after the trigger event *@param {Number} Time Call time (milliseconds) *@returns* /
  function trailingEdge(time) {
    timerId = undefined;
    // Determine whether invokeFunc needs to be invoked based on the trailing option, lastArgs
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }
  /** ** Cancel the timer */
  function cancel() {
    if(timerId ! = =undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }
  /** ** immediately triggers the handler *@returns* /
  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }
  /** ** returns an executable function * var debmentioning =function(){} *@returns* /
  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;
    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      //maxing set interval time (throttling mode)
      if (maxing) {
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        returninvokeFunc(lastCallTime); }}if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}
Copy the code

perfect

Have seen this code from Lodash, forgive me for running out of words… Since underscore cannot be as good as lodash and underscore, you should at least add the following:

  • Configure the function to execute immediately after the first trigger or after the last trigger event

  • Processing parameters

  • The return value

<html>
  <head>
    <title>resize</title>
    <script type="text/javascript">
      function resize() {
        console.log('Triggers the resize event for the zoom browser window.');
        return 'Resize is called.';
      }
      /** ** anti-shake function *@param {Function} Fn executes the function *@param {Number} Wait time *@param {Boolean} Immediate Executes *@returns Function Executable Function */
      function debounce(fn, wait, immediate) {
        var timerId = null;
        var result;
        // The timer is not added, so the event handler is triggered frequently.
        // If (fn) {// if (fn) {// if (fn) {// if (fn) {// if (fn) { You do it.
        return function () {
          var that = this;
          var args = arguments;
          if(timerId ! = =null) {
            clearTimeout(timerId); // Cancel the created timer
          }
          if (immediate) {
            if (timerId === null) {
              // Execute immediately
              result = fn.apply(that, args);
            }
            // After executing the event handler function immediately above, create the timer until the time setting timerId=null,
            // To ensure that the timerId will continue to be triggered during the wait
            timerId = setTimeout(function () {
              timerId = null;
            }, wait);
          } else {
            timerId = setTimeout(function () {
              // Even if the return value is received as result, it cannot be received externally (for the same reason as mentioned in debounce underscore)
              result = fn.apply(that, args);
            }, wait); // Set the timer
          }
          return result;
        };
      }
      Onresize receives an executable function. Because an executable is called each time the resize event is triggered, the return value of the debounce() function is an executable.
      window.onresize = debounce(resize, 3000.false);
    </script>
  </head>
  <body>
    <p>Changing the size of the browser window triggers the resize event.</p>
  </body>
</html>
Copy the code

Question and answer

As mentioned earlier, debounce is only executed once. Why can the LoDash version of Debounce be configured with an event handler for the first time and an event handler for the last time

The important thing again: Remember: Anti-shake is only done once!

The underscore function executes an event handler for the first time or after the last time the event is triggered. The third parameter, debounce, is used only to determine whether the implementation is implemented immediately or last There’s a case of 2.

Going back to the Lodash version of debounce, if you look closely you will see the default configuration of the third debounce parameter {leading:false,trailing {leading:true,trailing :true} is allowed only once because of lodash The throttling function is implemented by calling debounce, so it can be understood as {leading:true,trailing :true,maxWait:1000} where the two true and maxWait parameters are provided for the throttling function.

Therefore, the anti-shake or throttling function to be used in LoDash needs to be associated with the service scenario. The occurrence of {leading:false,trailing :true} mixed scenarios is minimized.

What are the differences between underscore and Lodash?

Underscore more direct looping uses trigger events -> cancel timers -> set timers; Lodash sets the timer by counting the time difference.

Underscore and throttling are divided into two separate functions that each perform their own tasks, whereas LoDash’s throttling functions are distinguished by configuration items that call underscore functions internally.

conclusion

Underscore lodash (debounce)