Debounce function implemented with promises

2019-02-09 10:56发布

问题:

I'm trying to implement a debounce function that works with a promise in javascript. That way, each caller can consume the result of the "debounced" function using a Promise. Here is the best I have been able to come up with so far:

function debounce(inner, ms = 0) {
  let timer = null;
  let promise = null;
  const events = new EventEmitter();  // do I really need this?

  return function (...args) {
    if (timer == null) {
      promise = new Promise(resolve => {
        events.once('done', resolve);
      });
    } else {
      clearTimeout(timer);
    }

    timer = setTimeout(() => {
      events.emit('done', inner(...args));
      timer = null;
    }, ms);

    return promise;
  };
}

Ideally, I would like to implement this utility function without introducing a dependency on EventEmitter (or implementing my own basic version of EventEmitter), but I can't think of a way to do it. Any thoughts?

回答1:

I found a better way to implement this with promises:

function debounce(inner, ms = 0) {
  let timer = null;
  let resolves = [];

  return function (...args) {    
    // Run the function after a certain amount of time
    clearTimeout(timer);
    timer = setTimeout(() => {
      // Get the result of the inner function, then apply it to the resolve function of
      // each promise that has been created since the last time the inner function was run
      let result = inner(...args);
      resolves.forEach(r => r(result));
      resolves = [];
    }, ms);

    return new Promise(r => resolves.push(r));
  };
}

I still welcome suggestions, but the new implementation answers my original question about how to implement this function without a dependency on EventEmitter (or something like it).



回答2:

I landed here because I wanted to get the return value of the promise, but debounce in underscore.js was returning undefined instead. I ended up using lodash version with leading=true. It works for my case because I don't care if the execution is leading or trailing.

https://lodash.com/docs/4.17.4#debounce

_.debounce(somethingThatReturnsAPromise, 300, {
  leading: true,
  trailing: false
})


回答3:

No clue what you are trying to accomplish as it vastly depends on what your needs are. Below is something somewhat generic though. Without a solid grasp of what is going on in the code below, you really might not want to use it though.

// Debounce state constructor
function debounce(f) {
  this._f = f;
  return this.run.bind(this)
}

// Debounce execution function
debounce.prototype.run = function() {
  console.log('before check');
  if (this._promise)
    return this._promise;
  console.log('after check');
  return this._promise = this._f(arguments).then(function(r) {
    console.log('clearing');
    delete this._promise; // remove deletion to prevent new execution (or remove after timeout?)
    return r;
  }.bind(this)).catch(function(r) {
    console.log('clearing after rejection');
    delete this._promise; // Remove deletion here for as needed as noted above
    return Promise.reject(r); // rethrow rejection
  })
}

// Some function which returns a promise needing debouncing
function test(str) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log('test' + str);
      resolve();
    }, 1000);
  });
}

a = new debounce(test); // Create debounced version of function
console.log("p1: ", p1 = a(1));
console.log("p2: ", p2 = a(2));
console.log("p1 = p2", p1 === p2);
setTimeout(function() {
  console.log("p3: ", p3 = a(3));
  console.log("p1 = p3 ", p1 === p3, " - p2 = p3 ", p2 === p3);
}, 2100)

View the console when running the code above. I put a few messages to show a bit about what is going on. First some function which returns a promise is passed as an argument to new debounce(). This creates a debounced version of the function.

When you run the debounced function as the code above does (a(1), a(2), and a(3)) you will notice during processing it returns the same promise instead of starting a new one. Once the promise is complete it removes the old promise. In code above I wait for the timeout manually with setTimeout before running a(3).

You can clear the promise in other ways as well, like adding a reset or clear function on debounce.prototype to clear the promise at a different time. You could also set it to timeout. The tests in the console log should show p1 and p2 get the same promise (reference comparison "===" is true) and that p3 is different.



回答4:

This is my implementation, only last call in interval will be resolved. In Chris's solution all calls will be resolved with delay between them, which is good, but sometimes we need resolve only last call. Correct me, if I'm wrong.

function debounce(f, interval) {
  let timer = null;

  return (...args) => {
    clearTimeout(timer);
    return new Promise((resolve) => {
      timer = setTimeout(
        () => resolve(f(...args)),
        interval,
      );
    });
  };
}


回答5:

This may not what you want, but can provide you some clue:

/**
 * Call a function asynchronously, as soon as possible. Makes
 * use of HTML Promise to schedule the callback if available,
 * otherwise falling back to `setTimeout` (mainly for IE<11).
 * @type {(callback: function) => void}
 */
export const defer = typeof Promise=='function' ? 
    Promise.resolve().then.bind(Promise.resolve()) : setTimeout;