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



我为Dynamics CRM REST/ODATA网络服务(CrmRestKit)开发了一个小型库。该库依赖于jQuery并利用promise模式,与jQuery的promise模式相同。

现在我喜欢将这个库移植到 bluebird 并删除 jQuery 依赖项。但是我遇到了一个问题,因为蓝鸟不支持承诺对象的同步解析。

一些上下文信息:

CrmRestKit 的 API 包含一个可选参数,该参数定义 Web 服务调用是否应在同步或异步模式下执行:

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

当您传递"true"或省略最后一个参数时,该方法将以同步模式创建记录。

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

我现在的问题如下:蓝鸟不支持同步模式下的分辨率。例如,当我执行以下操作时,"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 对象时,会立即调用 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 的思想是:无论执行模式(同步、异步)如何,调用方将始终处理一个 promise 对象。

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

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

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

感谢您的输入。

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

var currentUserRoels = null;
GetCurrentUserRoles().then(function(roles){
currentUserRoels = roles;
});
if( currentUserRoels.indexOf('foobar') === -1 ){
// ...
}

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

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

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

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

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

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

这个限制有点武断,坦率地说,我更喜欢 .NET 的观点。我会让其他人来决定他们是否认为以异步的方式执行可能同步或可能不同步的事情是"糟糕的代码"。

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

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

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

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

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

如果getSomeText函数可能是同步的或异步的,具体取决于上下文,则它不应影响执行顺序。你使用承诺来确保它总是相同的。具有可变的执行顺序可能会成为应用程序中的错误。

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

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

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

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

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

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

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

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

试试吧!

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

这将输出以下内容:

second
first 

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

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

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

底线:不要试图这样做。如果需要同步行为,请不要返回 promise。

有了这个,我将把你不知道JS的这句话留给你:

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

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

注意:有关Zalgo的更多信息,请参阅Oren Golan的"Don't Release 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"实际上是非常普遍和可靠的建议。始终保持异步。

这种情况呢,也与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);",而不必在使用此库的应用程序中更改原型?

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

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

最新更新