简体   繁体   English

如何取消 Javascript Promise 内的超时?

[英]How to cancel timeout inside of Javascript Promise?

I'm toying with promises in JavaScript and tried to promisify setTimeout function:我在玩弄 JavaScript 中的承诺,并试图承诺 setTimeout 函数:

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

Fairly straightforward but I was wondering how would I go about canceling my timeout before the promise resolves.相当简单,但我想知道如何在承诺解决之前取消我的超时。 timeout returns Promise object hence I loose access to value that setTimeout returns and cannot cancel timeout via clearTimeout . timeout返回Promise对象,因此我无法访问setTimeout返回的值,并且无法通过clearTimeout取消超时。 What woud be the best way to do it?最好的方法是什么?

BTW there is no real purpose for this, I just wonder how this would be approached.顺便说一句,这没有真正的目的,我只是想知道如何解决这个问题。 Also I plunked it here http://plnkr.co/edit/NXFjs1dXWVFNEOeCV1BA?p=preview我也把它放在这里http://plnkr.co/edit/NXFjs1dXWVFNEOeCV1BA?p=preview

Edit 2021 all platforms have converged on AbortController as the cancellation primitive and there is some built in support for this. Edit 2021所有平台都融合在 AbortController 作为取消原语,并且有一些内置支持。

In Node.js在 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();

In Browsers在浏览器中

You can polyfill this API and use the same as the example above:您可以 polyfill 这个 API 并使用与上面的示例相同的方法:


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

What you can do it that, you can return a canceller from your timeout function and invoke it when needed.你可以做什么,你可以从你的timeout函数返回一个取消器并在需要时调用它。 This way you do not need to store the timeoutid globally (or on the outer scope) and also this can manage multiple calls to the function as well.这样您就不需要全局(或在外部范围内)存储timeoutid并且这也可以管理对该函数的多次调用。 Each instance of the object return by the function timeout will have its own canceler that can perform the cancellation.函数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普林克

PSL's answer is right, however - there are a few caveats and I'd do it a bit differently.然而,PSL 的回答是正确的 - 有一些警告,我会做一些不同的事情。

  • A timeout being cleared means the code will not run - so we should reject the promise.超时被清除意味着代码不会运行 - 所以我们应该拒绝承诺。
  • Returning two things isn't necessary in our case, we can monkey patch in JavaScript.在我们的例子中不需要返回两个东西,我们可以在 JavaScript 中使用猴子补丁。

Here:这里:

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

Which would let us do:这会让我们做:

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"));

One might be interested in full blown cancellation and indeed some libraries support this directly as a feature of the library.人们可能对全面取消感兴趣,实际上有些图书馆直接支持这一点作为图书馆的一项功能。 In fact I'd dare say most do.事实上,我敢说大多数都这样做。 However, this causes interference problems.然而,这会导致干扰问题。 Quoting KrisKowal from here :这里引用 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.我现在确信 Promise 抽象本质上不可能取消( bg: that 传播),因为 Promise 可以有多个依赖项,并且可以随时引入依赖项。 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.另一种是引入一个新的抽象,一个可能是 thenable 的“任务”,作为交换,要求每个任务只有一个观察者(一个然后调用,永远),可以取消而不必担心干扰。 Tasks would support a fork() method to create a new task, allowing another dependee to retain the task or postpone cancellation.任务将支持 fork() 方法来创建新任务,允许另一个依赖者保留任务或推迟取消。

The above to answers by @Benjamin and @PSL work, but what if you need the cancelable timeout to be used by an outside source while being canceled internally?以上对@Benjamin 和@PSL 的回答是有效的,但是如果您需要在内部取消时由外部来源使用可取消超时怎么办?

For example, the interaction might look somewhat like this:例如,交互可能看起来像这样:

// externally usage of timeout 
async function() {
  await timeout() // timeout promise 
} 

// internal handling of timeout 
timeout.cancel() 

I needed this kind of implementation myself, so here's what I came up with:我自己需要这种实现,所以这是我想出的:

/**
 * 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)
}

Now you can use the timer like this:现在您可以像这样使用计时器:

let timeout = createTimer(100)

And have the promise canceled somewhere else:并在其他地方取消承诺:

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

This is my answer in TypeScript:这是我在 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);
    });
  }

Usage:用法:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM