如何取消Javascript Promise内部的超时?

How to cancel timeout inside of Javascript Promise?

我正在玩JavaScript中的Promise,并试图使setTimeout函数成问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
function timeout(ms) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('timeout done');
    }, ms);
  });
}

var myPromise=timeout(3000);

myPromise.then(function(result) {
  console.log(result); // timeout done
})

很简单,但是我想知道在Promise解决之前我应该??如何取消超时。 timeout返回Promise对象,因此我无法访问setTimeout返回的值,并且无法通过clearTimeout取消超时。最好的方法是什么?

顺便说一句,没有真正的目的,我只是想知道如何实现这一目标。我也在这里http://plnkr.co/edit/NXFjs1dXWVFNEOeCV1BA?p=preview


编辑2021,所有平台都已在AbortController上聚合为取消原语,对此有一些内置支持。

在Node.js中

1
2
3
4
5
6
7
8
9
10
11
// import { setTimeout } from 'timers/promises' // in ESM
const { setTimeout } = require('timers/promises');
const ac = new AbortController();

// cancellable timeout
(async () => {
  await setTimeout(1000, null, { signal: ac.signal });
})();

// abort the timeout, rejects with an ERR_ABORT
ac.abort();

在浏览器中

您可以多填充此API并使用与上面的示例相同的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function delay(ms, value, { signal } = {}) {
    return new Promise((resolve, reject) => {
        const listener = () => {
            clearTimeout(timer);
            reject(new Error('Aborted'));
        };
        const timer = setTimeout(() => {
            signal?.removeEventListener('abort', listener);
            resolve(value);
        }, ms);
        if (signal?.aborted) {
            listener();
        }
        signal?.addEventListener('abort', listener);
    });
}

您可以执行的操作是,您可以从timeout函数返回取消器,并在需要时调用它。这样,您就不必全局存储timeoutid(或在外部范围内),这也可以管理对该函数的多次调用。函数timeout返回的每个对象实例将具有其自己的取消程序,该取消程序可以执行取消操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function timeout(ms) {
  var timeout, promise;

  promise = new Promise(function(resolve, reject) {
    timeout = setTimeout(function() {
      resolve('timeout done');
    }, ms);
  });

  return {
           promise:promise,
           cancel:function(){clearTimeout(timeout );} //return a canceller as well
         };
}

var timeOutObj =timeout(3000);

timeOutObj.promise.then(function(result) {
  console.log(result); // timeout done
});

//Cancel it.
timeOutObj.cancel();

Plnkr


PSL的答案是正确的,但是-有一些警告,我会做一些不同的事情。

  • 清除超时意味着代码将无法运行-因此我们应该拒绝Promise。
  • 在我们的例子中,不需要返回两件事,我们可以在JavaScript中进行猴子补丁。

此处:

1
2
3
4
5
6
7
8
9
10
11
12
13
function timeout(ms, value) {
    var p = new Promise(function(resolve, reject) {
        p._timeout = setTimeout(function() {
            resolve(value);
        }, ms);
        p.cancel = function(err) {
            reject(err || new Error("Timeout"));
            clearTimeout(p._timeout); // We actually don't need to do this since we
                                      // rejected - but it's well mannered to do so
        };
    });
    return p;
}

哪个会让我们做:

1
2
3
4
5
6
7
8
9
var p = timeout(1500)
p.then(function(){
     console.log("This will never log");
})

p.catch(function(){
     console.log("This will get logged so we can now handle timeouts!")
})
p.cancel(Error("Timed out"));

一个人可能对完全取消感兴趣,实际上某些图书馆将其直接支持为图书馆的一项功能。实际上,我敢说大多数。但是,这引起干扰问题。从此处引用KrisKowal:

My position on cancellation has evolved. I am now convinced that cancellation (bg: that propagates) is inherently impossible with the Promise abstraction because promises can multiple dependess and dependees can be introduced at any time. If any dependee cancels a promise, it would be able to interfere with future dependees. There are two ways to get around the problem. One is to introduce a separate cancellation"capability", perhaps passed as an argument. The other is to introduce a new abstraction, a perhaps thenable"Task", which in exchange for requiring that each task only have one observer (one then call, ever), can be canceled without fear of interference. Tasks would support a fork() method to create a new task, allowing another dependee to retain the task or postpone cancellation.


上面由@Benjamin和@PSL回答的方法有效,但是如果您需要可取消的超时时间以在内部被取消时由外部来源使用该怎么办?

例如,交互可能看起来像这样:

1
2
3
4
5
6
7
// externally usage of timeout
async function() {
  await timeout() // timeout promise
}

// internal handling of timeout
timeout.cancel()

我自己需要这种实现,所以这是我想出的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Cancelable Timer hack.
 *
 *  @notes
 *    - Super() does not have `this` context so we have to create the timer
 *      via a factory function and use closures for the cancelation data.
 *    - Methods outside the consctutor do not persist with the extended
 *      promise object so we have to declare them via `this`.
 *  @constructor Timer
 */

function createTimer(duration) {
  let timerId, endTimer
  class Timer extends Promise {
    constructor(duration) {
      // Promise Construction
      super(resolve => {
        endTimer = resolve
        timerId = setTimeout(endTimer, duration)
      })
      // Timer Cancelation
      this.isCanceled = false
      this.cancel = function() {
        endTimer()
        clearTimeout(timerId)
        this.isCanceled = true
      }
    }
  }
  return new Timer(duration)
}

现在您可以像这样使用计时器:

1
let timeout = createTimer(100)

在其他地方取消Promise:

1
2
3
 if (typeof promise !== 'undefined' && typeof promise.cancel === 'function') {
  timeout.cancel()
}

这是我在TypeScript中的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  private sleep(ms) {
    let timerId, endTimer;
    class TimedPromise extends Promise {
      isCanceled: boolean = false;
      cancel = () => {
        endTimer();
        clearTimeout(timerId);
        this.isCanceled = true;
      };
      constructor(fn) {
        super(fn);
      }
    }
    return new TimedPromise(resolve => {
      endTimer = resolve;
      timerId = setTimeout(endTimer, ms);
    });
  }

用法:

1
2
3
const wait = sleep(10*1000);
setTimeout(() => { wait.cancel() },5 * 1000);
await wait;