【问题标题】:Synchronous promise resolution (bluebird vs. jQuery)同步承诺解析(bluebird vs. jQuery)
【发布时间】:2014-02-03 20:30:51
【问题描述】:

我为Dynamics CRM REST/ODATA webservice (CrmRestKit) 开发了一个小库。该库依赖于 jQuery,并利用了 promise-pattern,也就是 jQuery 的 promise-like-pattern。

现在我想将此库移植到 bluebird 并删除 jQuery 依赖项。但我面临一个问题,因为蓝鸟不支持承诺对象的同步解析。

一些上下文信息:

CrmRestKit 的 API 除外一个可选参数,该参数定义 Web 服务调用应以同步还是异步模式执行:

CrmRestKit.Create( 'Account', { Name: "foobar" }, false ).then( function ( data ) {
   ....
} );

当你传递“true”或省略最后一个参数时,方法是否会同步创建记录。模式。

有时需要在同步模式下执行操作,例如,您可以为 Dynamics CRM 编写 JavaScript 代码,该代码涉及表单的保存事件,并且在此事件处理程序中您需要执行同步操作用于验证(例如验证是否存在一定数量的子记录,如果存在正确数量的记录,则取消保存操作并显示错误消息)。

我现在的问题是:bluebird 不支持同步模式下的分辨率。例如,当我执行以下操作时,会以异步方式调用“then”处理程序:

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

///
/// 'Promise.cast' cast the given value to a trusted promise. 
///
function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return Promise.cast( text );
}

getSomeTextSimpleCast('first').then(print);
print('second');

输出如下:

print -> second
print -> first

我希望“第二个”出现在“第一个”之后,因为承诺已经用一个值解决了。所以我会假设一个 then-event-handler 当应用于已经解析的 promise-object 时会立即被调用。

当我用 jQuery 做同样的事情(在已经解决的承诺上使用 then)时,我会得到我预期的结果:

function jQueryResolved( opt_text ){

    var text = opt_text || 'jQuery-Test Value',
    dfd =  new $.Deferred();

    dfd.resolve(text);

        // return an already resolved promise
    return dfd.promise();
}

jQueryResolved('third').then(print);
print('fourth');

这将生成以下输出:

print -> third
print -> fourth

有没有办法让蓝鸟以同样的方式工作?

更新: 提供的代码只是为了说明问题。 lib 的想法是:无论执行模式(同步、异步)如何,调用者都将始终处理一个承诺对象。

关于“......询问用户......似乎没有任何意义”:当您提供两种方法“CreateAsync”和“CreateSync”时,用户也可以决定如何执行操作.

无论如何,对于当前实现,默认行为(最后一个参数是可选的)是异步执行。所以 99% 的代码需要一个 promise-object,可选参数仅用于 1% 的情况,即您只需要同步执行。此外,我为自己开发了 lib,并且在 99,9999% 的情况下使用异步模式,但我认为可以选择按照自己的喜好选择同步模式是件好事。

但我认为我明白了同步方法应该简单地返回值。对于下一个版本(3.0),我将实现“CreateSync”和“CreateAsync”。

感谢您的意见。

Update-2 我对可选参数的意图是确保一致的行为并防止逻辑错误。假设您是我使用 lib 的方法“GetCurrentUserRoles”的消费者。所以这个方法总是会返回一个promise,这意味着你必须使用“then”方法来执行依赖于结果的代码。所以当有人写这样的代码时,我同意这是完全错误的:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

if( currentUserRoels.indexOf('foobar') === -1 ){

    // ...
}

我同意当“GetCurrentUserRoles”方法从同步更改为异步时,此代码将中断。

但我知道这不是一个好的设计,因为消费者现在应该处理异步方法。

【问题讨论】:

  • 如果调用是同步的,为什么你会返回一个承诺?正常返回值即可。
  • 我想指出,以上评论来自Bluebird的作者。
  • @dystroy 不,它是调用者选择同步的重载函数(2 个不同的函数)。 API 也可以是CrmRestKit.CreateAsyncCrmRestKit.CreateSync,而不是CrmRestKit.Create({async: ?})。这与有时可能同步知道其返回值的函数完全不同(例如,用于数据库调用的一级缓存)
  • @thuld 绝对不是,同步函数必须返回直接值。如果您必须使用带有同步函数的 Promise,这是最糟糕的 API。 jQuery 所做的事情是非常错误的,因为当一个函数真正同步和异步时,它会导致不可预知的执行顺序——即调用者无法决定的时候。此外,从同步函数返回值和从异步函数返回值根本不是“不同的 API”。例如,查看 node fs API 或 XMLHttpRequest - 在使用同步调用时,它们都不会强制您使用回调或承诺。
  • OP,我认为您有足够的信息来接受其中一个答案或自行选择答案,不是吗?

标签: javascript jquery asynchronous promise bluebird


【解决方案1】:

简短版:我明白你为什么要这样做,但答案是否定的。

我认为要问的根本问题是,如果已完成的承诺已完成,是否应立即运行回调。我可以想到很多可能发生这种情况的原因 - 例如,异步保存过程仅在进行更改时才保存数据。它可能能够以同步方式检测来自客户端的更改,而无需通过外部资源,但如果检测到更改,那么只有在那时才需要异步操作。

在具有异步调用的其他环境中,该模式似乎是开发人员负责了解他们的工作可能会立即完成(例如,.NET 框架的异步模式实现就适应了这一点)。这不是框架的设计问题,而是它的实现方式。

JavaScript 的开发人员(以及上面的许多评论者)似乎对此有不同的看法,他们坚持认为,如果某些东西可能是异步的,那么它必须始终是异步的。这是否“正确”无关紧要 - 根据我在https://promisesaplus.com/ 找到的规范,第 2.2.4 项指出,在您脱离我所说的“脚本代码”之前,基本上不能调用回调或“用户代码”;也就是说,规范明确指出,即使承诺完成,您也不能立即调用回调。我检查了其他几个地方,他们要么对这个话题一言不发,要么同意原始来源。我不知道https://promisesaplus.com/ 是否可以被认为是这方面的权威信息来源,但我看到没有其他来源不同意它,它似乎是最完整的。

这个限制有点武断,坦率地说,我更喜欢 .NET 的观点。我会留给其他人来决定他们是否认为它是“糟糕的代码”,以一种看起来异步的方式执行可能是同步的,也可能不是同步的。

您的实际问题是是否可以将 Bluebird 配置为执行非 JavaScript 行为。性能方面,这样做可能会有一点好处,在 JavaScript 中,如果你足够努力,一切皆有可能,但随着 Promise 对象在平台上变得越来越普遍,你会看到将其用作本机组件而不是自定义编写的转变polyfills 或库。因此,无论今天的答案是什么,在 Bluebird 中重新编写一个 Promise 可能会在未来给您带来问题,并且您的代码可能不应该被编写为依赖于或提供对 Promise 的即时解决方案。

【讨论】:

  • 感谢您发布此解释。在找到这个之前,我们只花了一个小时在一个失败的规范上。 :(
【解决方案2】:

你可能认为这是个问题,因为没有办法拥有

getSomeText('first').then(print);
print('second');

当分辨率同步时,在"second" 之前打印getSomeText "first"

但我认为你的逻辑有问题。

如果您的getSomeText 函数可能是同步的异步的,具体取决于上下文,那么它不应影响执行顺序。你使用 Promise 来确保它总是一样的。执行顺序可变可能会成为应用程序中的错误。

使用

getSomeText('first') // may be synchronous using cast or asynchronous with ajax
.then(print)
.then(function(){ print('second') });

在这两种情况下(与cast 同步或异步解析),您将拥有正确的执行顺序。

请注意,函数有时是同步的,有时不是同步的,这并不是一个奇怪或不太可能的情况(想想缓存处理或池化)。你只需要假设它是异步的,一切都会好起来的。

但是,如果您不离开 JavaScript 领域(即,如果您不使用一些本机代码)。

【讨论】:

  • 代码的重点是有一个例子,同步承诺将导致“第一”“第二”。我的代码应该只显示我的问题的框架,而不是我使用 Promise 的方式。你的第二个代码示例是正确的,承诺将确保始终产生预期的打印顺序。
  • 真正重要的一点是,让代码依赖于立即执行的承诺是一个错误。没有承诺的同步解决将帮助您避免这种错误。
  • .then(以及回调调用延迟到下一个执行周期)的问题在于,它在大多数实现中增加了显着的延迟,使得 Promises 对于时间关键的过程(如动画)来说是不可接受的解决方案。例如,如果你有这样的东西:spinUp.then(rotateXtimes).then(slowDown),你会得到明显的动画中断,而像spinUp(function(){rotateXtimes(function(){slowDown(done)})}) 这样的简单回调的同步分辨率不会引入任何延迟。
  • @DenysSéguret “真正重要的一点是,代码依赖于立即执行的承诺是一个错误。”它并不总是一个错误。当数据已经可用并且您希望它同步,但您想重用现有的支持异步的代码路径时,有一些用例可以同步解决/执行回调。 (例如,我当前的用例:github.com/stacktracejs/stacktrace.js/issues/188
【解决方案3】:

promise 的目的是让 异步 代码更容易,即更接近您在使用 同步 代码时的感受。

您正在使用同步代码。不要让它变得更复杂。

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return text;
}

print(getSomeTextSimpleCast('first'));
print('second');

这应该是它的结束。


如果你想保持相同的异步接口,即使你的代码是同步的,那么你必须一直这样做。

getSomeTextSimpleCast('first')
    .then(print)
    .then(function() { print('second'); });

then 使您的代码脱离正常的执行流程,因为它应该是异步的。蓝鸟在那里以正确的方式做到了。简单解释一下它的作用:

function then(fn) {
    setTimeout(fn, 0);
}

请注意,bluebird 并没有真正做到这一点,它只是给你一个简单的例子。

试试吧!

then(function() {
    console.log('first');
});
console.log('second');

这将输出以下内容:

second
first 

【讨论】:

  • +1 因为我认为你做对了,但我做了另一个答案,因为我不确定从 OP 的角度来看你的答案是否真的很容易理解(当然我可能错了)。
  • @Florian Margaine 老实说,我更喜欢 jQuery 同步处理“then”的方式。例如,我使用它来构建动态事件链,其中后续函数依赖于前一个函数的结果,这些结果也根据某些标准而变化,并由共享的失败和完成函数处理。没有承诺的替代方案通常会导致一系列复杂的回调函数,很难遵循。不在这里敲 bb,我正在考虑迁移以获得性能优势。我只是不明白如果 then 始终是异步的,我将如何完成此操作。
  • @Brandon 你在说什么?如果你有同步函数,不要使用承诺 - 如果你有异步函数,jQuery deferreds 创建 race conditions 通过异步运行 sometimes 另一方面 Bluebird 总是运行无论种族如何,都一样
  • 没有代码示例有点难以表达,我不想劫持线程。基本上,多个依赖的“then”函数反对一个承诺,一旦安排了函数链,该函数就会解决。像 myPromise.then(functionA).then(dynamicFunc).then(functionD).fail(failFunction).done(doneFunction) 其中 dynamicFunc 可以是 functionB 或 functionC。然后只需调用 myPromise.resolve({data}) 即可开始。每个函数定义一个 deferred 并返回一个 promise(除了 done 和 fail)然后解析或拒绝 deferred 或者将数据传递给后续函数或调用失败。
  • 顺便说一句,你能解释一下“竞争条件”吗?可能举个例子?我似乎找不到太多关于为什么会发生这种情况的具体信息。如果您知道任何我希望看到的好资源。
【解决方案4】:

这里已经有一些很好的答案,但要非常简洁地总结问题的症结:

拥有一个时而异步时而同步的 Promise(或其他异步 API)是一件坏事。

您可能认为这很好,因为对 API 的初始调用需要一个布尔值来在同步/异步之间关闭。但是,如果它隐藏在一些包装代码中并且使用那个代码的人不知道这些恶作剧怎么办?他们只是因为自己没有过错而做出一些不可预测的行为。

底线:不要尝试这样做。如果您想要同步行为,请不要返回承诺。

有了这个,我会把You Don't Know JS的这段话留给你:

另一个信任问题被称为“为时过早”。在特定于应用程序的术语中,这实际上可能涉及在某些关键任务完成之前被调用。但更一般地说,问题在可以调用您现在(同步)或稍后(异步)提供的回调的实用程序中很明显。

这种围绕同步或异步行为的不确定性几乎总是会导致非常难以追踪错误。在某些圈子里,虚构的导致精神错乱的怪物 Zalgo 被用来描述同步/异步的噩梦。 “不要放开扎尔戈!”这是一种常见的呼声,它提供了非常合理的建议:始终异步调用回调,即使在事件循环的下一轮“立即”调用,这样所有回调都是可以预见的异步的。

注意:有关 Zalgo 的更多信息,请参阅 Oren Golan 的“不要释放 Zalgo!” (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) 和 Isaac Z. Schlueter 的“异步 API 设计”(http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony)。

考虑:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;`

这段代码会打印 0(同步回调调用)还是 1(异步回调调用)?取决于……条件。

您可以看到 Zalgo 的不可预测性能够以多快的速度威胁到任何 JS 程序。所以听起来很傻的“永远不要发布 Zalgo”实际上是非常普遍和可靠的建议。始终保持异步。

【讨论】:

  • +1 提到了这种不一致的麻烦,我正要自己去追踪 Zalgo 的文章(;
【解决方案5】:

这个案例呢,还有最新版本使用 Bluebird 的 CrmFetchKit 相关。我已经从基于 jQuery 的 1.9 版升级。使用 CrmFetchKit 的旧应用程序代码仍然具有我不能或不会更改其原型的方法。

现有应用代码

CrmFetchKit.FetchWithPaginationSortingFiltering(query.join('')).then(
    function (results, totalRecordCount) {
        queryResult = results;

        opportunities.TotalRecords = totalRecordCount;

        done();
    },
    function err(e) {
        done.fail(e);
    }
);

旧的 CrmFetchKit 实现(fetch() 的自定义版本)

function fetchWithPaginationSortingFiltering(fetchxml) {

    var performanceIndicator_StartTime = new Date();

    var dfd = $.Deferred();

    fetchMore(fetchxml, true)
        .then(function (result) {
            LogTimeIfNeeded(performanceIndicator_StartTime, fetchxml);
            dfd.resolve(result.entities, result.totalRecordCount);
        })
        .fail(dfd.reject);

    return dfd.promise();
}

新的 CrmFetchKit 实施

function fetch(fetchxml) {
    return fetchMore(fetchxml).then(function (result) {
        return result.entities;
    });
}

我的问题是旧版本有 dfd.resolve(...) ,我可以在其中传递我需要的任意数量的参数。

新的实现刚刚返回,父级好像调用了回调,不能直接调用。

我在新实现中制作了 fetch() 的自定义版本

function fetchWithPaginationSortingFiltering(fetchxml) {
    var thePromise = fetchMore(fetchxml).then(function (result) {
        thePromise._fulfillmentHandler0(result.entities, result.totalRecordCount);
        return thePromise.cancel();
        //thePromise.throw();
    });

    return thePromise;
}

但问题是回调被调用了两次,一次是我显式调用,第二次是由框架调用,但它只传递一个参数。为了欺骗它并“告诉”不要调用任何东西,因为我明确地这样做了,我尝试调用 .cancel() 但它被忽略了。我明白为什么,但你仍然如何做“dfd.resolve(result.entities, result.totalRecordCount);”在新版本中无需更改使用此库的应用程序中的原型?

【讨论】:

  • Promise 只能解析或拒绝单个值。您可以解析整个result 对象,然后将提供给.then() 的回调函数更改为仅接受result 对象作为参数。
  • 谢谢,看来这是唯一的方法。我确实修改了应用程序的代码以接受一个对象作为参数并从那里提取另外两个。我想这是将库升级到新版本时的重大变化。
【解决方案6】:

事实上,你可以这样做,是的。

修改 bluebird.js 文件(对于 npm:node_modules/bluebird/js/release/bluebird.js),进行以下更改:

[...]

    target._attachExtraTrace(value);
    handler = didReject;
}

- async.invoke(settler, target, {
+ settler.call(target, {
    handler: domain === null ? handler
        : (typeof handler === "function" &&

[...]

有关更多信息,请参阅此处:https://github.com/stacktracejs/stacktrace.js/issues/188

【讨论】:

  • 修改 bluebird 以打破对 Promises/A+ 规范的遵从性是个坏主意。想想以后需要调试您编写的代码的人......并且自然地假设 Promises/A+ 合规性。
猜你喜欢
  • 1970-01-01
  • 2016-07-27
  • 1970-01-01
  • 1970-01-01
  • 2018-02-01
  • 2015-10-23
  • 2013-10-26
  • 2017-04-08
  • 2015-05-11
相关资源
最近更新 更多