12

I have developed a small lib for the Dynamics CRM REST/ODATA webservice (CrmRestKit). The lib dependes on jQuery and utilizes the promise-pattern, repectivly the promise-like-pattern of jQuery.

Now I like to port this lib to bluebird and remove the jQuery dependency. But I am facing a problem because bluebird does not support the synchronous resolution of promise-objects.

Some context information:

The API of the CrmRestKit excepts an optional parameter that defines if the web-service call should be performed in sync or async mode:

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

When you pass "true" or omit the last parameter, will the method created the record in sync. mode.

Sometimes it is necessary to perform a operation in sync-mode, for instance you can write JavaScript code for Dynamics CRM that is involed for the save-event of an form and in this event-handler you need to perform sync-operation for validation (e.g. validate that a certain number of child-records exist, in case the right number of records exist, cancel the save-operation and show an error message).

My problem now is the following: bluebird does not support the resolution in sync-mode. For instance when I do the following, the "then" handler is invoked in async fashion:

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');

The output is the following:

print -> second
print -> first

I would expect that the "second" appears after the "first" because the promise is already resolved with an value. So I would assume that an then-event-handler is immediately invoked when applied on an already resolved promise-object.

When I do the same (use then on an already resolved promise) with jQuery I will have my expected result:

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');

This will generate the following output:

print -> third
print -> fourth

Is there a way to make bluebird work in the same fashion?

Update: The provided code was just to illustrate the problem. The idea of the lib is: Regardless of the execution-mode (sync, async) the caller will always deal with an promise-object.

Regarding "... asking the user... doesn't seems to make any sense": When you provide two methods "CreateAsync" and "CreateSync" it is also up to the user to decide how the operation is executed.

Anyway with the current implementation the default behavior (last parameter is optional) is a async execution. So 99% of the code requires a promise-object, the optional parameter is only use for the 1% cases where you simply need a sync execution. Furthermore I developed to lib for myself and I use in 99,9999% of the case the async mode but I thought it is nice to have the option to go the sync-road as you like.

But I thinks I got the point an sync method should simply return the value. For the next release (3.0) I will implement "CreateSync" and "CreateAsync".

Thanks for your input.

Update-2 My intension for the optional parameter was to ensure a consistend behavior AND prevent logic error. Assume your as a consumer of my methode "GetCurrentUserRoles" that uses lib. So the method will alway return an promise, that means you have to use the "then" method to execute code that depends on the result. So when some writes code like this, I agree it is totally wrong:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

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

    // ...
}

I agree that this code will break when the method "GetCurrentUserRoles" changes from sync to async.

But I understand that this I not a good design, because the consumer should now that he deals with an async method.

4

6 回答 6

20

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

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

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

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

这种限制有点武断,坦率地说,我更喜欢 .NET 的观点。我将由其他人来决定他们是否认为它是“糟糕的代码”,以一种看起来异步的方式来做一些可能同步或可能不同步的事情。

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

于 2015-01-15T19:30:06.250 回答
8

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

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

并且在分辨率同步getSomeText "first"之前打印过。"second"

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

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

利用

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

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

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

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

于 2014-01-16T08:36:26.070 回答
7

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 
于 2014-01-16T08:07:33.977 回答
3

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

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

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

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

有了这个,我会留下你不知道 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”实际上是非常普遍和可靠的建议。始终保持异步。

于 2015-02-12T21:45:41.130 回答
0

这个案例呢,还有与最新版本使用 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);” 在新版本中无需更改使用此库的应用程序中的原型?

于 2016-05-19T19:12:16.953 回答
-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

于 2017-04-11T11:36:39.510 回答