我为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”方法从同步更改为异步时,此代码将中断。
但我明白这不是一个好的设计,因为消费者现在应该处理异步方法。
答案 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