使用promises实现的去抖功能

时间:2016-02-05 15:42:46

标签: javascript promise

我正在尝试实现一个与javascript中的promise一起使用的去抖功能。这样,每个调用者都可以使用Promise消耗“debounced”函数的结果。这是迄今为止我能够提出的最好的:

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;
  };
}

理想情况下,我想实现这个实用程序函数,而不会引入依赖于EventEmitter(或实现我自己的基本版本的EventEmitter),但我想不出办法。有什么想法吗?

9 个答案:

答案 0 :(得分:21)

我找到了一个更好的方法来实现承诺:

TableName

我仍然欢迎建议,但是新的实现回答了我关于如何在不依赖于EventEmitter(或类似的东西)的情况下实现此函数的原始问题。

答案 1 :(得分:5)

这是我的实现,只有间隔内的最后一个调用会得到解决。在克里斯的解决方案中,所有呼叫将在它们之间延迟解决,这很好,但是有时我们只需要解决最后一个呼叫。纠正我,如果我错了。

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

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

答案 2 :(得分:4)

我登陆这里是因为我希望获得承诺的返回值,但是在underscore.js中的debounce正在返回undefined。我最终使用lodash版本,其中leading = true。它适用于我的情况,因为我不关心执行是领先还是尾随。

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

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

答案 3 :(得分:2)

如果有人需要,这是我的打字稿版本(主要基于克里斯一个)?

function promiseDebounce (exec: (...args: any[]) => Promise<any>, interval: number): () => ReturnType<typeof exec> {
    let handle: number | undefined;
    let resolves: Array<(value?: unknown) => void> = [];

    return async (...args: unknown[]) => {
        clearTimeout(handle);
        handle = setTimeout(
            () => {
                const result = exec(...args);
                resolves.forEach(resolve => resolve(result));
                resolves = [];
            },
            interval
        );

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

答案 4 :(得分:0)

不知道你想要完成什么,因为它在很大程度上取决于你的需求。下面是一些有点通用的东西。如果没有牢固掌握下面代码中的内容,你可能真的不想使用它。

&#13;
&#13;
// 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)
&#13;
&#13;
&#13;

运行上面的代码时查看控制台。我发了几条消息来说明发生了什么。首先,一些返回promise的函数作为参数传递给new debounce()。这会创建该函数的去抖动版本。

当您按照上面的代码(a(1), a(2), and a(3))运行去抖动函数时,您会注意到在处理过程中它返回相同的承诺而不是开始新的承诺。一旦承诺完成,它就会删除旧承诺。在上面的代码中,我在运行a(3)之前使用setTimeout手动等待超时。

您也可以通过其他方式清除承诺,例如在debounce.prototype上添加重置或清除功能以在不同时间清除承诺。您也可以将其设置为超时。控制台日志中的测试应该显示p1和p2得到相同的承诺(参考比较&#34; ===&#34;是真的)并且p3是不同的。

答案 5 :(得分:0)

这可能不是您想要的,但是可以为您提供一些线索:

/**
 * 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;

答案 6 :(得分:0)

这是我想出的解决此问题的方法。批处理到同一调用的所有去抖动功能的调用都返回相同的Promise,以解决将来的调用结果。

function makeFuture() {
  let resolve;
  let reject;
  let promise = new Promise((d, e) => {
    resolve = d;
    reject = e;
  });
  return [promise, resolve, reject];
}

function debounceAsync(asyncFunction, delayMs) {
  let timeout;
  let [promise, resolve, reject] = makeFuture();
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(async () => {
      const [prevResolve, prevReject] = [resolve, reject];
      [promise, resolve, reject] = makeFuture();
      try {
        prevResolve(await asyncFunction.apply(this, args));
      } catch (error) {
        prevReject(error);
      }
    }, delayMs);
    return promise;
  }
}

const start = Date.now();
const dog = {
  sound: 'woof',
  bark() {
    const delay = Date.now() - start;
    console.log(`dog says ${this.sound} after ${delay} ms`);
    return delay;
  },
};
dog.bark = debounceAsync(dog.bark, 50);
Promise.all([dog.bark(), dog.bark()]).then(([delay1, delay2]) => {
  console.log(`Delay1: ${delay1}, Delay2: ${delay2}`);
});

答案 7 :(得分:0)

Chris和НиколайГордеев都有很好的解决方案。第一个解决所有问题。问题是它们全部得到解决,但通常您不希望所有这些都运行。

第二个解决方案解决了这个问题,但创建了一个新问题-现在您将有多个等待。如果该函数被称为很多函数(例如搜索类型),则可能存在内存问题。我通过创建以下asyncDebounce来解决此问题,该问题将解决最后一个并拒绝(并且await ing调用将获得一个他们可以捕获的异常)。


const debounceWithRejection = (
  inner,
  ms = 0,
  reject = false,
  rejectionBuilder
) => {
  let timer = null;
  let resolves = [];

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      const resolvesLocal = resolves;
      resolves = [];
      if (reject) {
        const resolve = resolvesLocal.pop();
        resolve.res(inner(...args));
        resolvesLocal.forEach((r, i) => {
          !!rejectionBuilder ? r.rej(rejectionBuilder(r.args)) : r.rej(r.args);
        });
      } else {
        resolvesLocal.forEach((r) => r.res(inner(...args)));
      }
      resolves = [];
    }, ms);
    return new Promise((res, rej) =>
      resolves.push({ res, rej, args: [...args] })
    );
  };
};

拒绝逻辑是可选的,rejectionBuilder也是如此。您可以选择拒绝特定的构建者,这样您就知道要抓住它。

您可以看到runing example

答案 8 :(得分:0)

解决一个承诺,取消其他承诺

我见过的许多实现都使问题过于复杂或存在其他卫生问题。在这篇文章中,我们将编写自己的 debounce。此实现将 -

  • 在任何给定时间(每个去抖动任务)至多有一个待处理的 promise
  • 通过正确取消挂起的承诺来阻止内存泄漏
  • 仅解析最新的 promise
  • 通过实时代码演示展示正确的行为

我们用它的两个参数编写 debouncetask 去抖动,以及延迟的毫秒数,ms。我们为其本地状态引入了一个本地绑定,t -

function debounce (task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return async (...args) => {
    try {
      t.cancel()
      t = deferred()
      await t.promise
      await task(...args)
    }
    catch (_) { /* prevent memory leak */ }
  }
}

我们依赖于一个可重用的 deferred 函数,该函数创建了一个在 ms 毫秒内解析的新承诺。它引入了两个本地绑定,promise 本身,cancel 它的能力 -

function deferred (ms) {
  let cancel, promise = new Promise((resolve, reject) => {
    cancel = reject
    setTimeout(resolve, ms)
  })
  return { promise, cancel }
}

点击计数器示例

在第一个示例中,我们有一个按钮来计算用户的点击次数。事件侦听器使用 debounce 附加,因此计数器仅在指定的持续时间后递增 -

// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }

// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter

// event handler
function clickCounter (event) {
  mycounter.value = Number(mycounter.value) + 1
}

// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>

实时查询示例,“自动完成”

在第二个示例中,我们有一个带有文本输入的表单。我们的 search 查询使用 debounce -

附加

// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }

// dom references
const myform = document.forms.myform
const myresult = myform.myresult

// event handler
function search (event) {
  myresult.value = `Searching for: ${event.target.value}`
}

// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))
<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>