如何在一定时间后使承诺超时? 我知道 Q 有一个 Promise 超时,但我使用的是原生 NodeJS Promise,而且它们没有 .timeout 函数。
我是否漏掉了一个或者它的包装方式不同?
或者,下面的实现在不占用内存方面是否良好,实际上按预期工作?
我还可以以某种方式将其全局包装,以便我可以将它用于我创建的每个 Promise,而不必重复 setTimeout 和clearTimeout 代码吗?
function run() {
logger.info('DoNothingController working on process id {0}...'.format(process.pid));
myPromise(4000)
.then(function() {
logger.info('Successful!');
})
.catch(function(error) {
logger.error('Failed! ' + error);
});
}
function myPromise(ms) {
return new Promise(function(resolve, reject) {
var hasValueReturned;
var promiseTimeout = setTimeout(function() {
if (!hasValueReturned) {
reject('Promise timed out after ' + ms + ' ms');
}
}, ms);
// Do something, for example for testing purposes
setTimeout(function() {
resolve();
clearTimeout(promiseTimeout);
}, ms - 2000);
});
}
谢谢!
原生 JavaScript Promise 没有任何超时机制。
关于您的实现的问题可能更适合http://codereview.stackexchange.com,但有一些注意事项:
您没有提供实际执行承诺中任何操作的方法,并且
在
clearTimeout
回调中不需要 setTimeout
,因为 setTimeout
安排了一个一次性计时器。
由于一旦解决/拒绝承诺就无法解决/拒绝,因此您不需要该检查。
所以继续你的
myPromise
函数方法,也许是这样的:
function myPromise(timeout, callback) {
return new Promise((resolve, reject) => {
// Set up the timeout
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout} ms`));
}, timeout);
// Set up the real work
callback(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
});
}
这样使用:
myPromise(2000, (resolve, reject) => {
// Real work is here
});
(或者可能稍微不那么复杂,请参见下面的分隔线。)
我有点担心语义不同这一事实(没有
new
,而您确实将 new
与 Promise
构造函数一起使用)。但它更大的问题是它假设你总是从头开始创建一个承诺,但你通常希望能够使用你已经拥有的承诺。
您可以通过子类化来处理这两个问题
Promise
:
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
if (haveTimeout) {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
init(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
} else {
init(resolve, reject);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
用法(如果构建新的 Promise):
let p = new MyPromise(300, (resolve, reject) => {
// ...
});
p.then((value) => {
// ...
})
.catch((error) => {
// ...
});
用法(如果使用您已有的承诺):
MyPromise.resolveWithTimeout(100, somePromiseYouAlreadyHave)
.then((value) => {
// ...
})
.catch((error) => {
// ...
});
实例:
"use strict";
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
if (haveTimeout) {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
init(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
} else {
init(resolve, reject);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
// Some functions for the demonstration
const neverSettle = () => new Promise(() => {});
const fulfillAfterDelay = (delay, value) => new Promise((resolve) => setTimeout(resolve, delay, value));
const rejectAfterDelay = (delay, error) => new Promise((resolve, reject) => setTimeout(reject, delay, error));
const examples = [
function usageWhenCreatingNewPromise1() {
console.log("Showing timeout when creating new promise");
const p = new MyPromise(100, (resolve, reject) => {
// We never resolve/reject, so we test the timeout
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenCreatingNewPromise2() {
console.log("Showing when the promise is fulfilled before the timeout");
const p = new MyPromise(100, (resolve, reject) => {
setTimeout(resolve, 50, "worked");
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenCreatingNewPromise3() {
console.log("Showing when the promise is rejected before the timeout");
const p = new MyPromise(100, (resolve, reject) => {
setTimeout(reject, 50, new Error("failed"));
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise1() {
console.log("Showing timeout when using a promise we already have");
return MyPromise.resolveWithTimeout(100, neverSettle())
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise2() {
console.log("Showing fulfillment when using a promise we already have");
return MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"))
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise3() {
console.log("Showing rejection when using a promise we already have");
return MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")))
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
async function usageInAnAsyncFunction1() {
console.log("Showing timeout in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, neverSettle());
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
async function usageInAnAsyncFunction2() {
console.log("Showing fulfillment in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"));
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
async function usageInAnAsyncFunction3() {
console.log("Showing rejection in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")));
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
];
(async () => {
for (const example of examples) {
try {
await example();
} catch (e) {
}
}
})();
/* Shows the cosole full height in the snippet */
.as-console-wrapper {
max-height: 100% !important;
}
上面的代码在解决或拒绝 Promise 时主动取消计时器。根据您的用例,这可能不是必需的,并且会使代码有点复杂。对于事物的承诺部分来说,这不是必需的;一旦承诺被解决或拒绝,就无法更改,再次调用
resolve
或 reject
函数对承诺没有影响(规范对此很清楚)。但是,如果您不取消计时器,则计时器仍处于待处理状态,直到它触发为止。例如,待处理的承诺将阻止 Node.js 退出,因此,如果您在执行此操作时在最后一件事上设置了很长的超时,则可能会毫无意义地延迟退出进程。浏览器不会延迟离开带有挂起计时器的页面,因此这不适用于浏览器。同样,您的里程可能会有所不同,您可以通过不取消计时器来简化一些。
如果你不关心待处理的计时器,
MyPromise
会更简单:
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
init(resolve, reject);
if (haveTimeout) {
setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
要为任何现有的 Promise 添加超时,您可以使用:
const withTimeout = (millis, promise) => {
let timeoutPid;
const timeout = new Promise((resolve, reject) =>
timeoutPid = setTimeout(
() => reject(`Timed out after ${millis} ms.`),
millis));
return Promise.race([
promise,
timeout
]).then(result) {
if (timeoutPid) {
clearTimeout(timeoutPid);
}
return result;
});
};
然后:
await withTimeout(5000, doSomethingAsync());
虽然可能不支持承诺超时,但您可以竞争承诺:
var race = Promise.race([
new Promise(function(resolve){
setTimeout(function() { resolve('I did it'); }, 1000);
}),
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, 800);
})
]);
race.then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
});
通用
Promise.timeout
:
Promise.timeout = function(timeout, cb){
return Promise.race([
new Promise(cb),
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, timeout);
})
]);
}
示例:
Promise.timeout = function(timeout, cb) {
return Promise.race([
new Promise(cb),
new Promise(function(resolve, reject) {
setTimeout(function() {
reject('Timed out');
}, timeout);
})
]);
}
function delayedHello(cb){
setTimeout(function(){
cb('Hello');
}, 1000);
}
Promise.timeout(800, delayedHello).then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
}); //delayedHello doesn't make it.
Promise.timeout(1200, delayedHello).then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
}); //delayedHello makes it.
可能成本有点高,因为你实际上创建了 3 个承诺,而不是 2 个。不过我认为这样更清楚。
您可能想要设置一个承诺,而不是让函数为您构建它。通过这种方式,您可以分离关注点,并最终专注于将您的 Promise 与新建的 Promise 进行竞赛,该 Promise 将在
x
毫秒内被拒绝。
Promise.timeout = function(timeout, promise){
return Promise.race([
promise,
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, timeout);
})
]);
}
使用方法:
var p = new Promise(function(resolve, reject){
setTimeout(function() { resolve('Hello'); }, 1000);
});
Promise.timeout(800, p); //will be rejected, as the promise takes at least 1 sec.
这是一个有点老的问题,但是当我寻找如何使承诺超时时,我偶然发现了这个问题。
虽然所有答案都很棒,但我发现使用 Promises 的 bluebird 实现作为 处理超时的最简单方法:
var Promise = require('bluebird');
var p = new Promise(function(reject, resolve) { /.../ });
p.timeout(3000) //make the promise timeout after 3000 milliseconds
.then(function(data) { /handle resolved promise/ })
.catch(Promise.TimeoutError, function(error) { /handle timeout error/ })
.catch(function(error) { /handle any other non-timeout errors/ });
如您所见,这比其他建议的解决方案要少得多。我想我会把它放在这里以便人们更容易找到它:)
顺便说一句,我绝对没有参与蓝鸟项目,只是发现这个特定的解决方案非常简洁。
扩展方法是一个方便的解决方案:
fetch("/example")
.withTimeout(5000);
这是通过向 Promise 全局对象的 prototype 添加一个方法来实现的。
promiseWithTimeout.js
:
/** Adds a timeout (in milliseconds) that will reject the promise when expired. */
Promise.prototype.withTimeout =
function (milliseconds) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("Timeout")), milliseconds);
return this
.then(value => {
clearTimeout(timeout);
resolve(value);
})
.catch(exception => {
clearTimeout(timeout);
reject(exception);
});
});
};
export {}; // only needed if imported as a module
对于 TypeScript 支持,请在定义之前添加以下声明块
Promise.prototype.withTimeout
:
declare global {
interface Promise<T> {
/** Adds a timeout (in milliseconds) that will reject the promise when expired. */
withTimeout(milliseconds: number): Promise<T>;
}
}
如果您的代码放置在类中,您可以使用装饰器。你在 utils-decorators 库中有这样的装饰器 (
npm install --save utils-decorators
):
import {timeout} from 'utils-decorators';
class SomeService {
@timeout(3000)
doSomeAsync(): Promise<any> {
....
}
}
或者你可以使用包装函数:
import {timeoutify} from 'utils-decorators';
const withTimeout = timeoutify(originalFunc, 3000);
虽然这里的答案是有效的,但您不应该尝试重新发明轮子,而应该使用 NPM 上数十个可用软件包之一来实现自我解决的承诺。
这是来自 NPM 的一个示例:
const { TimeoutResolvePromise, TimeoutRejectPromise } = require('nodejs-promise-timeout');
const TIMEOUT_DELAY = 2000;
// This promise will reject after 2 seconds:
let promise1 = new TimeoutRejectPromise(TIMEOUT_DELAY, (resolve, reject) => {
// Do something useful here, then call resolve() or reject()
});
在这种情况下包装纸会很方便
const result = await withTimeout(() => doSomethingAsync(...args), 3000)();
或
const result = await withTimeout(doSomethingAsync, 3000)(...args);
甚至
const doSomethingAsyncWithTimeout = withTimeout(doSomethingAsync, 3000);
const result = await doSomethingAsyncWithTimeout(...args);
/**
* returns a new function which calls the input function and "races" the result against a promise that throws an error on timeout.
*
* the result is:
* - if your async fn takes longer than timeout ms, then an error will be thrown
* - if your async fn executes faster than timeout ms, you'll get the normal response of the fn
*
* ### usage
* ```ts
* const result = await withTimeout(() => doSomethingAsync(...args), 3000);
* ```
* or
* ```ts
* const result = await withTimeout(doSomethingAsync, 3000)(...args);
* ```
* or even
* ```ts
* const doSomethingAsyncWithTimeout = withTimeout(doSomethingAsync, 3000);
* const result = await doSomethingAsyncWithTimeout(...args);
* ```
*/
const withTimeout = <R, P extends any, T extends (...args: P[]) => Promise<R>>(logic: T, ms: number) => {
return (...args: Parameters<T>) => {
// create a promise that rejects in <ms> milliseconds; https://italonascimento.github.io/applying-a-timeout-to-your-promises/
const timeout = new Promise((resolve, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(new Error(`promise was timed out in ${ms} ms, by withTimeout`));
}, ms); // tslint:disable-line align
});
// returns a "race" between our timeout and the function executed with the input params
return Promise.race([
logic(...args), // the wrapped fn, executed w/ the input params
timeout, // the timeout
]) as Promise<R>;
};
};