同步承诺解析(bluebird vs. jQuery)

时间:2014-01-15 11:31:44

标签: javascript jquery asynchronous promise bluebird

我为Dynamics CRM REST/ODATA webservice(CrmRestKit)开发了一个小型库。 lib依赖于jQuery并使用promise模式,后者是jQuery的类似promise的模式。

现在我想将此lib移植到bluebird并删除jQuery依赖项。但是我遇到了一个问题,因为bluebird不支持promise-objects的同步解析。

一些上下文信息:

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

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

当我使用jQuery执行相同的操作(然后使用已经解决的承诺)时,我会得到预期的结果:

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的思想是:无论执行模式(sync,async)如何,调用者总是会处理promise-object。

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

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

但我认为我得到的一点是同步方法应该只返回值。对于下一个版本(3.0),我将实现“CreateSync”和“CreateAsync”。

感谢您的意见。

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

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

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

    // ...
}

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

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

6 个答案:

答案 0 :(得分:17)

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

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

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

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

这种限制有些武断,我坦率地更喜欢.NET的观点。我会把它留给其他人来决定他们是否认为“坏代码”做一些可能会或可能不会以看起来异步的方式同步的东西。

您的实际问题是Bluebird是否可以配置为执行非JavaScript行为。在性能方面,这样做可能会带来一些小好处,而在JavaScript中,如果你足够努力就可以做任何事情,但随着Promise对象在各个平台上变得越来越普遍,你会看到转向使用它作为本机组件而不是自定义编写polyfill或库。因此,无论今天的答案是什么,在Bluebird中重新实现承诺可能会在将来导致您出现问题,并且您的代码可能不应该依赖于或立即解决承诺。

答案 1 :(得分:8)

您可能认为这是一个问题,因为没有办法

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

并且在getSomeText分辨率同步时打印"first" "second"

但我认为你有一个逻辑问题。

如果getSomeText函数可能是同步异步,则根据上下文,它不应影响执行顺序。你使用promises来确保它始终是一样的。具有可变执行顺序可能会成为应用程序中的错误。

使用

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

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

请注意,让某个函数有时是同步的,有时候不是一个奇怪或不太可能的情况(考虑缓存处理或池化)。你只需假设它是异步的,一切都会好的。

但是如果你不希望操作是异步的,那么要求API的用户精确地使用布尔参数似乎没有任何意义,如果你不离开JavaScript的领域(即如果你不使用一些本地代码)。

答案 2 :(得分:7)

承诺的目的是使异步代码更容易,即更接近使用同步代码时的感觉。

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

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 

答案 3 :(得分:2)

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

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

您可能认为这很好,因为对API的初始调用需要布尔值才能在同步/异步之间关闭。但是,如果将其隐藏在某些包装器代码中并且使用 代码的人不知道这些恶作剧怎么办?他们刚刚完成了一些不可靠的行为,而不是他们自己的过错。

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

有了这个,我会从You Don't Know JS

给你这个引用
  

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

     

这种围绕同步或异步行为的不确定性几乎总是会导致很难追踪错误。在某些圈子中,名为Zalgo的虚构的疯狂诱发怪物被用来描述同步/异步噩梦。 “不要释放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”实际上是非常普遍和坚实的建议。永远是异步。

答案 4 :(得分:0)

这个案例怎么样,CrmFetchKit也与最近版本使用Bluebird相关。我已经从基于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);”在新版本中,不必在使用此库的应用程序中更改原型?

答案 5 :(得分:-1)

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

修改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