Discussion on throttling and anti shake function of JavaScript

  • 2021-08-06 20:52:42
  • OfStack

Concept

Throttling function

Execute the incoming method at regular intervals

The purpose is to prevent the function from executing too fast and affecting the performance. It is common to bind functions with scrolling and mouse movement events.

Anti-shake function

For those who have come into contact with hardware, it may be better understood that when the hardware button is pressed, the current fluctuation will be triggered many times because the user presses and holds it for different times, and only once will be triggered by adding an anti-shake function, thus preventing the problems caused by meaningless current fluctuation.

Why does the key anti-rebound (Debounce) want to shake? When the mechanical key is pressed, it does not touch well when pressed, especially the mechanical switch with reed will open and close repeatedly at the moment of contact until the switch state changes completely.

When applied in the front end, the common scene is that the query/search/verification is triggered after the typing action of the input box ends for a period of time, instead of triggering every typing word, resulting in meaningless ajax query, etc., or a function bound to adjust the window size, in fact, only needs to execute the action after the last window size is fixed.

Self-realization

Anti-shake function

The key point is to empty the handle of the delay function every time it is triggered, Only the last trigger will not empty the handle, so the last trigger will wait for the default 1s to execute the parameter function f passed in from debounce. The closure function returned from debounce is really triggered every time it is called, instead of the original f, so arguments here takes arguments in the closure function environment variable and passes it to f when executing f, and gets it outside setTimeout function.


let debounce = function(f, interval = 1000) {
 let handler = null;
 return function() {
  if (handler) {
  clearTimeout(handler);
  }
  let arg = arguments;
  handler = setTimeout(function() {
  f.apply(this, arg);
  clearTimeout(handler);
  }, interval)
 }
 }

Application:


let input = document.querySelector('#input');
 input.addEventListener('input', debounce(function(e) {
 console.log(" Your input is ",e.target.value)
 }))

A more advanced implementation will also take into account, with leading and trailing as parameters, whether to execute the function once first and eliminate the following jitter, or finally execute the function once to eliminate the previous jitter, as in my example here. The anti-shake function of loadash will be analyzed in detail later.

Throttling function


let throttle = function(f,gap = 300){
  let lastCall = 0;
  return function(){
  let now = Date.now();
  let ellapsed = now - lastCall;
  if(ellapsed < gap){
   return
  }
  f.apply(this,arguments);
  lastCall = Date.now();
  }
 }

Closure function in the continuous call period, to record the interval from the last call time, if the interval time is less than the throttle set time, directly return, not to execute the real wrapped function f. Only the interval time is greater than the throttle set time gap, just call f, and update the call time.

Application:


document.addEventListener('scroll', throttle(function (e) {
 //  Logic to determine whether to scroll to the bottom 
 console.log(e,document.documentElement.scrollTop);
 }));

lodash source code analysis

The above is the most basic and simple implementation of throttling anti-shake function. We will analyze the throttling anti-shake function in lodash library under 1.

Use of throttle function


$(window).on('scroll', _.debounce(doSomething, 200));

function debounce(func, wait, options) {
 var lastArgs,
  lastThis,
  result,
  timerId,
  lastCallTime = 0,
  lastInvokeTime = 0,
  leading = false,
  maxWait = false,
  trailing = true;

 if (typeof func != 'function') {
  throw new TypeError(FUNC_ERROR_TEXT);
 }
 wait = wait || 0;
 if (isObject(options)) {
  leading = !!options.leading;
  maxWait = 'maxWait' in options && Math.max((options.maxWait) || 0, wait);
  trailing = 'trailing' in options ? !!options.trailing : trailing;
 }

 function invokeFunc(time) {
  var args = lastArgs,
  thisArg = lastThis;

  lastArgs = lastThis = undefined;
  lastInvokeTime = time;
  result = func.apply(thisArg, args);
  return result;
 }

 function leadingEdge(time) {
  console.log("leadingEdge setTimeout")
  // Reset any `maxWait` timer.
  lastInvokeTime = time;
  // Start the timer for the trailing edge.
  timerId = setTimeout(timerExpired, wait);
  // Invoke the leading edge.
  return leading ? invokeFunc(time) : result;
 }

 function remainingWait(time) {
  var timeSinceLastCall = time - lastCallTime,
  timeSinceLastInvoke = time - lastInvokeTime,
  result = wait - timeSinceLastCall;
  console.log("remainingWait",result)
  return maxWait === false ? result : Math.min(result, maxWait - timeSinceLastInvoke);
 }

 function shouldInvoke(time) {
  console.log("shouldInvoke")
  var timeSinceLastCall = time - lastCallTime,
  timeSinceLastInvoke = time - lastInvokeTime;
  console.log("time",time,"lastCallTime",lastCallTime,"timeSinceLastCall",timeSinceLastCall)
  console.log("time",time,"lastInvokeTime",lastInvokeTime,"timeSinceLastInvoke",timeSinceLastInvoke)
  console.log("should?",(!lastCallTime || (timeSinceLastCall >= wait) ||
  (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait)))
  // Either this is the first call, activity has stopped and we're at the
  // trailing edge, the system time has gone backwards and we're treating
  // it as the trailing edge, or we've hit the `maxWait` limit.
  return (!lastCallTime || (timeSinceLastCall >= wait) ||
  (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait));
 }

 function timerExpired() {
  console.log("timerExpired")
  var time = Date.now();
  if (shouldInvoke(time)) {
  return trailingEdge(time);
  }
  console.log("Restart the timer.",time,remainingWait(time))
  // Restart the timer.
  console.log("timerExpired setTimeout")
  timerId = setTimeout(timerExpired, remainingWait(time));
 }

 function trailingEdge(time) {
  clearTimeout(timerId);
  timerId = undefined;

  // Only invoke if we have `lastArgs` which means `func` has been
  // debounced at least once.
  console.log("trailing",trailing,"lastArgs",lastArgs)
  if (trailing && lastArgs) {
  return invokeFunc(time);
  }
  lastArgs = lastThis = undefined;
  return result;
 }

 function cancel() {
  if (timerId !== undefined) {
  clearTimeout(timerId);
  }
  lastCallTime = lastInvokeTime = 0;
  lastArgs = lastThis = timerId = undefined;
 }

 function flush() {
  return timerId === undefined ? result : trailingEdge(Date.now());
 }

 function debounced() {
  var time = Date.now(),
  isInvoking = shouldInvoke(time);
  console.log("time",time);
  console.log("isInvoking",isInvoking);
  lastArgs = arguments;
  lastThis = this;
  lastCallTime = time;

  if (isInvoking) {
  if (timerId === undefined) {
   return leadingEdge(lastCallTime);
  }
  // Handle invocations in a tight loop.
  clearTimeout(timerId);
  console.log("setTimeout")
  timerId = setTimeout(timerExpired, wait);
  return invokeFunc(lastCallTime);
  }
  return result;
 }
 debounced.cancel = cancel;
 debounced.flush = flush;
 return debounced;
 }

ref

https://css-tricks.com/debouncing-throttling-explained-examples/

https://github.com/lodash/lodash/blob/4.7.0/lodash.js#L9840

https://jinlong.github.io/2016/04/24/Debouncing-and-Throttling-Explained-Through-Examples/

The above is about the details of JavaScript throttling and anti-shake function. For more information about JavaScript throttling and anti-shake function, please pay attention to other related articles on this site!


Related articles: