如何在JavaScript Promise中取消超时?

32

我正在尝试使用 JavaScript 中的 Promise 并尝试将 setTimeout 函数转换为 Promise:

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


您也可以使用装饰器,更多细节请参见此处:https://dev59.com/_FwY5IYBdhLWcg3wlohg#61242606 - vlio20
4个回答

33

2021年修改:所有平台都已经使用AbortController作为取消原语,并且已经内置了对此的支持。

在Node.js中

// 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();

在浏览器中

您可以使用 polyfill 实现该 API 并将其用作上面示例中的代码:


function delay(ms, value, { signal } = {}) {
    return new Promise((resolve, reject) => {
        const listener = () => {
            clearTimeout(timer);
            reject(signal.reason);
        };
        signal?.throwIfAborted();
        const timer = setTimeout(() => {
            signal?.removeEventListener('abort', listener);
            resolve(value);
        }, ms);
        signal?.addEventListener('abort', listener);
    });
}

你可以这样做,可以从你的timeout函数中返回一个取消器,并在需要时调用它。这样你就不需要全局地(或外部作用域)存储timeoutid,同时还可以管理对该函数的多个调用。由函数timeout返回的对象的每个实例将具有自己的取消器,可执行取消操作。


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

22

PSL的回答是正确的,但是有一些注意事项,我会稍微不同地处理。

  • 清除超时意味着代码将不会运行 - 因此我们应该拒绝这个promise。
  • 在我们的情况下,返回两个东西是不必要的,我们可以在JavaScript中进行monkey patch。

这里:

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

这将使我们能够:

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的话:(来源)

我的立场关于取消已经发生了变化。我现在相信,使用Promise抽象来传递取消是不可能的,因为Promise可以有多个依赖项和依赖者可以随时引入。如果任何依赖者取消Promise,它将能够干扰未来的依赖者。解决该问题有两种方法。一种是引入一个单独的取消“能力”,可能作为参数传递。另一种是引入一个新的抽象,即可交换的“任务”,以换取每个任务只有一个观察者(一个then调用,永远不变),可以在不担心干扰的情况下取消。任务将支持一个fork()方法来创建一个新的任务,允许另一个依赖者保留任务或推迟取消。


4
虽然我找不到相关的记录,但 Promise 的 "settler" 函数似乎会在 var p = new Promise() 同一个事件轮次中运行,因此您不能在 settler 中引用 p。解决方案(至少是我能想到的唯一一个)相当丑陋,但可以解决问题 - 演示链接 - Roamer-1888
因此,这是一个更好的解决方案。 - Roamer-1888
作为引文的补充,kriskowal在这里提出了更多的想法(https://github.com/kriskowal/gtor/blob/master/cancelation.md)。 - Clark Pan
2
@Benjamin-Gruenbaum 的回答更好。如果您向 Promise 添加成员,则将无法从依赖 Promise(即 .then() 结果)取消;有关更多信息,请参见此文章:https://blog.codecentric.de/en/2015/03/cancelable-async-operations-promises-javascript/ - arolson101
5
在定时器函数中,出现了 TypeError: Cannot set property '_timeout' of undefined 的错误。 - dd619

3
@Benjamin和@PSL提供的答案是可行的,但如果您需要在内部取消的同时使外部来源使用可取消的超时功能呢?
例如,交互可能看起来像这样:
// externally usage of timeout 
async function() {
  await timeout() // timeout promise 
} 

// internal handling of timeout 
timeout.cancel() 

我自己也需要这种实现方式,以下是我想出的方法:
/**
 * 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)
}

现在您可以像这样使用计时器:
let timeout = createTimer(100)

并在其他地方取消承诺:

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

1
这是我的 TypeScript 回答:
  private sleep(ms) {
    let timerId, endTimer;
    class TimedPromise extends Promise<any> {
      isCanceled: boolean = false;
      cancel = () => {
        endTimer();
        clearTimeout(timerId);
        this.isCanceled = true;
      };
      constructor(fn) {
        super(fn);
      }
    }
    return new TimedPromise(resolve => {
      endTimer = resolve;
      timerId = setTimeout(endTimer, ms);
    });
  }

使用方法:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接