Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't Convert Debounce ES5 to ES6

I found a great debounce() function code online, but I am having a hard time converting it from ES5 to ES6.

The issue is as follows: When I use the ES5 implementation, everything works perfectly. The window is resized, console.log() is immediately triggered, and subsequent resizings are ignored until after the 500ms I specified.

However, in the ES6 implementation, the first call works immediately... but every time after that it is also delayed by 500ms, even after cooling down!

If there is anyone who knows what I am doing wrong, I would really appreciate some help.

Examples:

function debounceES5(func, wait, immediate) {
    var timeout;
    return function () {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

const debounceES6 = (callback, wait, immediate=false) => {
  let timeout = null
  return (...args) => {
    const callNow = immediate && !timeout
    const next = () => callback(...args)
    clearTimeout(timeout)
    timeout = setTimeout(next, wait)
    if (callNow) { next() }
  }
}

document.querySelector('.es5').addEventListener('click', debounceES5(() => {console.log('clicked')}, 1000, true))

document.querySelector('.es6').addEventListener('click', debounceES6(() => {console.log('clicked')}, 1000, true))
Click both of these buttons fast and see how they react differently

<br />

<button class="es5">ES5</button>
<button class="es6">ES6</button>
like image 257
Hybrid Avatar asked May 06 '19 23:05

Hybrid


People also ask

What is Debounced?

Debouncing is a programming pattern or a technique to restrict the calling of a time-consuming function frequently, by delaying the execution of the function until a specified time to avoid unnecessary CPU cycles, and API calls and improve performance.

What is denouncing in JavaScript?

The debounce() function forces a function to wait a certain amount of time before running again. The function is built to limit the number of times a function is called. The Send Request() function is debounced. Requests are sent only after fixed time intervals regardless of how many times the user presses the button.


3 Answers

Here's your ES5 function written in ES6 - without skipping ANY details (except the context which is not relevant)

const debounce = (func, wait, immediate=false) => {
    let timeout;
    return (...args) => {
        const later = () => {
            timeout = null; // added this to set same behaviour as ES5
            if (!immediate) func(...args); // this is called conditionally, just like in the ES5 version
        };
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func(...args);
    };
};

Note, the later function does exactly what the ES5 version does

This should now behave (almost) identically to the ES5 version ... the whole context = this thing seems completely weird in the ES5 version anyway makes sense with example usage

However, as per comments, since the code is used for event handler, this is quite important, it's the Event Target so, you really can't return an arrow function

BEtter code would be

const debounce = (func, wait, immediate=false) => {
    let timeout;
    return function (...args) {
        const later = () => {
            timeout = null; // added this to set same behaviour as ES5
            if (!immediate) func.apply(this, args); // this is called conditionally, just like in the ES5 version
        };
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(this, args);
    };
};
like image 149
Jaromanda X Avatar answered Sep 30 '22 05:09

Jaromanda X


You can't use arrow functions in debounce (well, you need to know where you can and where you can't)

Arrow function bind this when they are created. That means the this in an array will never and can never change.

For example

'use strict';

function safeToString(v) {
  return v === undefined 
      ? 'undefined' 
      : v.toString();
}

function regularFunc() {
  console.log('regular:', safeToString(this));
}

const arrowFunc = () => {
  console.log('arrow:', safeToString(this));
};

regularFunc();
arrowFunc();

regularFunc.call("hello");
arrowFunc.call("world");

Notice in the regularFunc case this is undefined and later we can redefine it to hello but in the arrowFunc case it's always [Object Window]

So no to your ES6 debounce. Here's The supposedly working ES6 version

const debounce = (callback, wait, immediate) => {
    let timeout;
    return (...args) => {
      const callNow = immediate && !timeout
      const next = () => callback(...args)
      clearTimeout(timeout)
      timeout = setTimeout(next, wait)
      if (callNow) { next() }
    }
  }

Let's test it

function es5Debounce(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this,
      args = arguments;
    var later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
};

const es6Debounce = (callback, wait, immediate) => {
  let timeout;
  return (...args) => {
    const callNow = immediate && !timeout
    const next = () => callback(...args)
    clearTimeout(timeout)
    timeout = setTimeout(next, wait)
    if (callNow) {
      next()
    }
  }
}

function safeToString(v) {
  return v === undefined
      ? 'undefined'
      : v.toString();
}

class Test {
  constructor(name) {
    this.name = name;
  }
  log(...args) {
    console.log(
        this ? this.name : 'undefined', 
        safeToString(this),
        ...args);
  }
}

class ES5Test extends Test {
  constructor() {
    super('ES5:');
  }
}
ES5Test.prototype.log = es5Debounce(ES5Test.prototype.log, 1);

class ES6Test extends Test {
  constructor() {
    super('ES6:');
  }
}
ES6Test.prototype.log = es6Debounce(ES6Test.prototype.log, 1);

const es5Test = new ES5Test();
const es6Test = new ES6Test();

es5Test.log("hello");
es6Test.log("world");

As you can see the es6 version fails because this is wrong. If you only ever use non-member functions then es6Debounce will look like it's working but as soon as you use member functions on a class or event handlers you'll see es6Debounce does not work, this is not set correctly.

The code here tries to show the error. ES5Class and ES6Class are identical. The test should print

ES5: [object Object] hello
ES6: [object Object] world

instead it prints

ES5: [object Object] hello
undefined undefined world

As another example let's try an event handler

function es5Debounce(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this,
      args = arguments;
    var later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
};

const es6Debounce = (callback, wait, immediate) => {
  let timeout;
  return (...args) => {
    const callNow = immediate && !timeout
    const next = () => callback(...args)
    clearTimeout(timeout)
    timeout = setTimeout(next, wait)
    if (callNow) {
      next()
    }
  }
}

function mousemove(e) {
  console.log(this.id, e.pageX, e.pageY);
}

document.querySelector('#es5')
    .addEventListener('mousemove', es5Debounce(mousemove));
document.querySelector('#es6')
    .addEventListener('mousemove', es6Debounce(mousemove));
#es5, #es6 {
  margin: 1em;
  width: 10em;
  height: 2em;
  display: inline-block;
}
#es5 {
  background: orange;
}
#es6 {
  background: yellow;
}
<div id="es5">es5</div>
<div id="es6">es6</div>

Move the mouse over the 2 areas. Notice again the es6 one is wrong.

Whether or not that's important do you I have no idea but the original debounce you posted explicitly has code to make that case work.

like image 41
gman Avatar answered Sep 30 '22 04:09

gman


My preferred solution is the following one taken from Chris Boakes' blog post Understanding how a JavaScript ES6 debounce function works:

function debounce(callback, wait) {
    let timeout;
    return (...args) => {
        const context = this;
        clearTimeout(timeout);
        timeout = setTimeout(() => callback.apply(context, args), wait);
    };
}

Although it does not offer immediate arg. is still good enough for it to be shared here.

like image 37
Lucio Avatar answered Sep 30 '22 05:09

Lucio