在 JavaScript 中,如何在超时中包装承诺?
Posted
技术标签:
【中文标题】在 JavaScript 中,如何在超时中包装承诺?【英文标题】:in JavaScript, how to wrap a promise in timeout? 【发布时间】:2014-08-02 17:22:36 【问题描述】:使用 defered/promise 来实现一些异步函数的超时是一种常见的模式:
// Create a Deferred and return its Promise
function timeout(funct, args, time)
var dfd = new jQuery.Deferred();
// execute asynchronous code
funct.apply(null, args);
// When the asynchronous code is completed, resolve the Deferred:
dfd.resolve('success');
setTimeout(function()
dfd.reject('sorry');
, time);
return dfd.promise();
现在我们可以执行一些名为myFunc
的异步函数并处理超时:
// Attach a done and fail handler for the asyncEvent
$.when( timeout(myFunc, [some_args], 1000) ).then(
function(status)
alert( status + ', things are going well' );
,
function(status)
alert( status + ', you fail this time' );
);
好的,让我们在这个故事中有所改变!想象一下 myFunc
本身返回了一个承诺(注意:承诺没有延迟,我无法更改它):
function myFunc()
var dfd = new jQuery.Deffered();
superImportantLibrary.doSomething(function(data))
if(data.length < 5)
dfd.reject('too few data');
else
dfd.resolve('success!');
, 'error_callback': function()
dfd.reject("there was something wrong but it wasn't timeout");
);
return dfd.promise();
现在,如果我将myFunc
包装在timeout
中,我将失去处理与超时不同的错误的能力。如果myFunc
发出进度事件,我也会放弃它。
所以问题是:如何修改timeout
函数以便它可以接受返回承诺的函数而不会丢失它们的错误/进度信息?
【问题讨论】:
您的原语是错误的,您需要分两个阶段对其进行承诺,首先 - 承诺 superImportantLibrary.doSomething 方法,然后才执行承诺返回。另外,请避免使用 jQuery 承诺,与其他实现相比,它们很糟糕。 @BenjaminGruenbaum - 哪些实现?为什么 jQuery 的 promise 很可怕?你说'你的原语是错误的'是什么意思?如果它是库而不是我自己的代码,我如何“承诺”superImportantLibrary.doSomething
,你能写一些示例代码来解释你的意思吗?
我担心我无法在没有证明自己的情况下做出这样的声明:) 所以this is how to convert an API to promises(转换库本身)、this is why jQuery deferreds are bad 和as explained by domenic,至于图书馆,我会使用Bluebird
@BenjaminGruenbaum - 所以myFunc
是一种承诺superImportantLibrary.doSomething
方法的方法。它只返回一个承诺。为什么说是错的?我仍然会很感激一些代码来解释你将如何以正确的方式做到这一点。感谢其他链接!
我已经添加了一个答案,如果您不确定任何事情,请随时要求澄清。
【参考方案1】:
我知道这是 2 岁,但万一有人正在寻找答案......
我认为 Benjamin 很接近,因为您希望单独处理超时,因此我们将从他的延迟函数开始。
function delay(ms)
var d = $.Deferred();
setTimeout(function() d.resolve(); , ms);
return d.promise();
然后,如果您想在代码执行之前等待,您可以调用您希望由于此承诺而延迟的方法。
function timeout(funct, args, time)
return delay(time).then(function()
// Execute asynchronous code and return its promise
// instead of the delay promise. Using "when" should
// ensure it will work for synchronous functions as well.
return $.when(funct.apply(null, args));
);
这通常是我在寻找复习时想要做的事情(为什么我在这里)。但是,问题不在于延迟执行,而是如果执行时间过长则抛出错误。在这种情况下,这会使事情变得复杂,因为如果你不需要等待超时,你就不想等待,所以你不能只将两个承诺包装在“何时”中。看起来我们需要另一个deferred。 (见Wait for the first of multiple jQuery Deferreds to be resolved?)
function timeout(funct, args, time)
var d = $.Deferred();
// Call the potentially async funct and hold onto its promise.
var functPromise = $.when(funct.apply(null, args));
// pass the result of the funct to the master defer
functPromise.always(function()
d.resolve(functPromise)
);
// reject the master defer if the timeout completes before
// the functPromise resolves it one way or another
delay(time).then(function()
d.reject('timeout');
);
// To make sure the functPromise gets used if it finishes
// first, use "then" to return the original functPromise.
return d.then(function(result)
return result;
);
我们可以简化这一点,因为在这种情况下,master defer 只有在超时首先发生时才会拒绝,并且只有在 functPromise 首先解决时才会解决。因此,我们不需要将 functPromise 传递给 master defer resolve,因为它是唯一可以传递的东西,而且我们仍在范围内。
function timeout(funct, args, time)
var d = $.Deferred();
// Call the potentially async funct and hold onto its promise.
var functPromise = $.when(funct.apply(null, args))
.always(d.resolve);
// reject the master defer if the timeout completes before
// the functPromise resolves it one way or another
delay(time).then(function()
d.reject('timeout');
);
// To make sure the functPromise gets used if it finishes
// first, use "then" to return the original functPromise.
return d.then(function()
return functPromise;
);
【讨论】:
【参考方案2】:function timeout(funct, args, time)
var deferred = new jQuery.Deferred(),
promise = funct.apply(null, args);
if (promise)
$.when(promise)
.done(deferred.resolve)
.fail(deferred.reject)
.progress(deferred.notify);
setTimeout(function()
deferred.reject();
, time);
return deferred.promise();
【讨论】:
不。您不能在返回承诺的函数上调用reject
或 resolve
。抱歉,我无法更改 myFunc
以返回延迟而不是承诺...
我认为你可以跳过if(deferred.state() === 'pending')
,因为如果延迟已经被拒绝或解决,deferred.reject()
将什么都不做。
@JoeBrockhaus - 好吧,审稿人错了(这种情况时有发生)。删除 if(deferred.state() === 'pending')
只是删除死代码并且不会以任何方式更改功能,因为 deferred.reject()
已经在内部进行了自己的检查,如果状态不是 pending
(根据承诺规范),则不会做任何事情。哦,好吧。
谢谢大家,我已经修好了。
调用$.Deferred()
时需要new
吗?我没有这样做。【参考方案3】:
您应该始终以尽可能低的水平承诺。让我们从基础开始。
我将在这里使用 jQuery Promise,但这应该通过像 Bluebird 这样更强大的库来完成让我们从简单的开始,创建我们的 delay
为:
function delay(ms)
var d = $.Deferred();
setTimeout(function() d.resolve(); , ms);
return d.promise();
注意延迟并没有做任何令人惊讶的事情,我们的延迟函数所做的只是导致ms
毫秒的延迟。
现在,对于您的库,我们想要创建一个与 Promise 一起使用的 doSomething
版本:
superImportantLibrary.doSomethingAsync = function()
var d = $.Deferred();
superImportantLibrary.doSomething(function(data) d.resolve(data); );
return d.promise();
;
请注意我们的 delay 和 doSomethingAsync
函数都只做一件事。现在乐趣开始了。
function timeout(promise,ms)
var timeout = delay(ms); // your timeout
var d = $.Deferred();
timeout.then(function() d.reject(new Error("Timed Out")); );
promise.then(function(data) d.resolve(data); );
return d.promise();
timeout(superImportantLibrary.doSomethingAsync(),1000).then(function(data)
// handle success of call
, function(err)
// handle timeout or API failure.
);
现在在 Bluebird 中,整个代码应该是:
superImportantLibrary.doSomethingAsync().timeout(1000).then(function()
// complete and did not time out.
);
【讨论】:
是的,但你也可以写timeout(myFunc(), 1000).then(...)
,所以我真的不明白你为什么坚持用doSomethingAsync
代替它。除此之外,在promise.then(...)
部分中,应该有处理错误的代码和进度,但我知道你把它作为实现细节留下了。
如果promise
被拒绝了怎么办?进度通知呢?我感觉到延迟的反模式:-) 最干净的解决方案可能类似于Promise.race(promise, rejectAfterDelay(ms))
。
@Bergi 是肯定的,最干净的解决方案就是我在 Bluebird 中最新所说的。问题是 jQuery 没有 .race
方法或任何类似的方法,因此实现它会非常难看。
doSomethingAsync
只是包装库函数,不是您对长度等的验证检查。记住,promise 是安全的。以上是关于在 JavaScript 中,如何在超时中包装承诺?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 Typescript 认为 async/await 返回包装在承诺中的值?
尽管使用 done,Mocha 在 Before 钩子中调用异步承诺链超时