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

【讨论】:

不。您不能在返回承诺的函数上调用 rejectresolve。抱歉,我无法更改 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 中,如何在超时中包装承诺?的主要内容,如果未能解决你的问题,请参考以下文章

javascript 承诺超时

为啥 Typescript 认为 async/await 返回包装在承诺中的值?

如何等待超时的承诺?

尽管使用 done,Mocha 在 Before 钩子中调用异步承诺链超时

如何在Javascript中制作精确的睡眠功能,可能使用承诺?

即使我只有一个参数,我也会收到“包装承诺不可迭代错误”