Разрешаване на синхронно обещание (bluebird срещу jQuery)

Разработих малка библиотека за Dynamics CRM REST/ODATA уеб услуга (CrmRestKit). Библиотеката зависи от jQuery и използва модела на обещанието, респективно подобен на обещанието модел на jQuery.

Сега ми харесва да пренеса тази библиотека към bluebird и да премахна зависимостта от jQuery. Но се сблъсквам с проблем, защото bluebird не поддържа синхронното разрешаване на обекти на обещания.

Малко контекстна информация:

API на CrmRestKit изключва незадължителен параметър, който определя дали извикването на уеб услугата трябва да се извърши в синхронизиран или асинхронен режим:

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

Когато подадете "true" или пропуснете последния параметър, методът ще създаде записа в синхрон. режим.

Понякога е необходимо да се извърши операция в режим на синхронизиране, например можете да напишете JavaScript код за Dynamics CRM, който е включен за събитието за запазване на формуляр и в този манипулатор на събития трябва да извършите операция за синхронизиране за валидиране ( например потвърдете, че съществува определен брой дъщерни записи, в случай че съществува правилният брой записи, отменете операцията за запазване и покажете съобщение за грешка).

Проблемът ми сега е следният: 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

Бих очаквал, че "вторият" се появява след "първия", защото обещанието вече е разрешено със стойност. Така че бих предположил, че тогавашният манипулатор на събития се извиква незабавно когато се приложи върху вече разрешен обект на обещание.

Когато направя същото (използвам след това върху вече разрешено обещание) с 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

Има ли начин bluebird да работи по същия начин?

Актуализация: Предоставеният код беше само за да илюстрира проблема. Идеята на lib е: независимо от режима на изпълнение (sync, async), извикващият винаги ще работи с обещаващ обект.

Относно „... питането на потребителя... изглежда няма смисъл“: Когато предоставите два метода „CreateAsync“ и „CreateSync“, потребителят също трябва да реши как да се изпълни операцията.

Във всеки случай с текущата реализация поведението по подразбиране (последният параметър е незадължителен) е асинхронно изпълнение. Така че 99% от кода изисква обещаващ обект, незадължителният параметър се използва само за 1% от случаите, когато просто се нуждаете от синхронизиране. Освен това разработих lib за себе си и използвам в 99,9999% от случаите асинхронен режим, но реших, че е хубаво да имаш опцията да вървиш по пътя на синхронизирането, както желаеш.

Но мисля, че разбрах смисъла, че методът за синхронизиране трябва просто да върне стойността. За следващото издание (3.0) ще внедря "CreateSync" и "CreateAsync".

Благодаря за вашето мнение.

Актуализация-2 Моето намерение за незадължителния параметър беше да осигуря последователно поведение И да предотвратя логическа грешка. Да приемем, че сте потребител на моя метод "GetCurrentUserRoles", който използва lib. Така че методът винаги ще връща обещание, което означава, че трябва да използвате метода "then", за да изпълните код, който зависи от резултата. Така че, когато някой пише код като този, съгласен съм, че е напълно погрешен:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

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

    // ...
}

Съгласен съм, че този код ще се счупи, когато методът „GetCurrentUserRoles“ се промени от sync на async.

Но разбирам, че това не е добър дизайн, защото потребителят би трябвало сега, когато се занимава с асинхронен метод.


person thuld    schedule 15.01.2014    source източник
comment
Защо, за бога, ще върнете обещание, ако обаждането е синхронно? Просто върнете стойността нормално.   -  person Esailija    schedule 15.01.2014
comment
Бих искал да отбележа, че горният коментар е от автора на Bluebird.   -  person Domenic    schedule 15.01.2014
comment
@Esailija Мисля, че OP искаше да има функция, която понякога е синхронна, а понякога не, и да скрие тази точка на внедряване отвън.   -  person Denys Séguret    schedule 16.01.2014
comment
@dystroy не, това е претоварена функция (2 различни функции), при която повикващият избира синхронност. API може също така да бъде CrmRestKit.CreateAsync и CrmRestKit.CreateSync вместо CrmRestKit.Create({async: ?}). Това е напълно различно от функция, която понякога може да знае своята върната стойност синхронно (напр. кеш от първо ниво за извикване на база данни)   -  person Esailija    schedule 16.01.2014
comment
@Esailija Какво имаш предвид е, че API на OP (CrmRestKit.Create(..., sync)) няма смисъл? Ако е така, съгласен съм.   -  person Denys Séguret    schedule 16.01.2014
comment
@dystroy Искам да кажа, че извикването на метод като CreateSync() няма смисъл да връща обещание, тъй като това е нормално синхронно извикване. Същото претоварване на параметър като OP се използва в XMLHttpRequest, което може да бъде заменено с openAsync() и openSync(), но вашият оригинален коментар бърка това с метод, който понякога се връща синхронно. Това не е същото, защото претоварването при async = false винаги се връща синхронно, така че в този случай няма нужда от обещание.   -  person Esailija    schedule 16.01.2014
comment
@Esailija Мисля, че реалният случай на използване на OP е за функция, която понякога се връща асинхронно: API на CrmRestKit изключва незадължителен параметър, който определя дали извикването на уеб услугата трябва да се изпълнява в синхронизиран или асинхронен режим. Но ако OP просто зареже въпроса си и не дойде да ни просветли, може просто да си загубим времето...   -  person Denys Séguret    schedule 16.01.2014
comment
@dystroy, който не понякога се връща асинхронно. Повикващият избира кой метод да извика с асинхронния параметър. Те или извикват синхронния метод, или асинхронния метод. Това е точно като използването на синхронен XHR - няма да използвате обратните извиквания, а само .responseText директно.   -  person Esailija    schedule 16.01.2014
comment
Е, това ескалира бързо... Идеята е следната: Независимо дали операцията се изпълнява async или sync, трябва винаги да имате един и същ API -› ще получите обещание. @Esailija: Прав си, това е претоварване на функцията. Моля, вземете под внимание, че примерният код във въпроса е само моята детска площадка, за да разбера bluebird. Моля, разгледайте документа на моя проект: crmrestkit.codeplex.com/documentation   -  person thuld    schedule 16.01.2014
comment
@thuld абсолютно не, функциите за синхронизиране трябва да връщат директни стойности. Най-лошият API, ако трябва да използвате обещания със синхронни функции. Това, което прави jQuery, е много погрешно, защото ще доведе до непредсказуем ред на изпълнение, когато дадена функция наистина е както синхронна, така и асинхронна - това е, когато извикващият не може да реши. Също така изобщо не е различен API за връщане на стойности от синхронни функции и обещания от асинхронни. Погледнете API на възел fs или XMLHttpRequest например - нито един от тях не ви принуждава да използвате обратни извиквания или обещания, когато използвате синхронни повиквания.   -  person Esailija    schedule 16.01.2014
comment
Няма разлика между наличието на булев параметър и отделни методи CreateAsync и CreateSync - потребителят избира и в случай на булев параметър. Това се опитвам да обясня.   -  person Esailija    schedule 19.01.2014
comment
OP, мисля, че имате достатъчно информация, за да приемете един от отговорите или да посочите свой собствен, нали?   -  person Denys Séguret    schedule 05.02.2014
comment
@Esailija Независимо дали е добър дизайн (мисля, че сте прав, че не е), може да има случаи, в които заявката на OP дава практическа полза -- когато има съществуваща библиотека с дълбока верига от извиквания на функции, базирани на обещания, и в някои случаи знаете, че всички данни вече са там и просто искате да ги извлечете синхронно, за да ги върнете от функция или нещо подобно. В този случай, модифицирането на системата Promise за незабавно разрешаване, ако е възможно, позволява тази функционалност да бъде постигната без модифициране на всички десетки функции във веригата в рамките на библиотеката.   -  person Venryx    schedule 11.04.2017


Отговори (6)


Кратка версия: Разбирам защо искате да направите това, но отговорът е не.

Мисля, че основният въпрос, който се задава, е дали завършено обещание трябва незабавно да изпълни обратно извикване, ако обещанието вече е изпълнено. Мога да се сетя за много причини това да се случи - например асинхронна процедура за запис, която записва данни само ако са направени промени. Може да е в състояние да открие промени от страна на клиента по синхронен начин, без да се налага да преминава през външен ресурс, но ако промените бъдат открити тогава и само тогава ще се изисква асинхронна операция.

В други среди, които имат асинхронни извиквания, моделът изглежда е, че разработчикът е отговорен за разбирането, че тяхната работа може да завърши незабавно (например внедряването на асинхронния шаблон на .NET framework се съобразява с това). Това не е проблем с дизайна на рамката, а начинът, по който е внедрена.

Разработчиците на JavaScript (и много от коментаторите по-горе) изглежда имат различна гледна точка по този въпрос, настоявайки, че ако нещо може да е асинхронно, то винаги трябва да бъде асинхронно. Дали това е „правилно“ или не е без значение – според спецификацията, която намерих на https://promisesaplus.com/, точка 2.2.4 заявява, че по принцип не могат да бъдат извикани обратни извиквания, докато не излезете от това, което ще наричам „код на скрипта“ или „потребителски код“; т.е. спецификацията казва ясно, че дори ако обещанието е изпълнено, не можете да извикате обратното извикване веднага. Проверих на няколко други места и те или не казват нищо по темата, или са съгласни с първоизточника. Не знам дали https://promisesaplus.com/ може да се счита за окончателен източник на информация в това отношение, но няма други източници, които Видях, че не съм съгласен с него и изглежда, че е най-пълният.

Това ограничение е донякъде произволно и аз честно казано предпочитам .NET перспективата за това. Ще оставя на другите да решат дали смятат за „лош код“ да направят нещо, което може или не може да бъде синхронно по начин, който изглежда асинхронен.

Вашият действителен въпрос е дали Bluebird може да бъде конфигуриран да изпълнява поведението, което не е JavaScript. По отношение на производителността може да има незначителна полза от това, а в JavaScript всичко е възможно, ако се постараете достатъчно, но тъй като обектът Promise става все по-разпространен в платформите, ще видите преминаване към използването му като роден компонент вместо персонализиран написан полифили или библиотеки. Като такъв, какъвто и да е отговорът днес, преработването на обещание в Bluebird вероятно ще ви създаде проблеми в бъдеще и вашият код вероятно не трябва да бъде написан така, че да зависи от или да предоставя незабавно разрешаване на обещание.

person Joe Friesenhan    schedule 15.01.2015
comment
Благодаря, че публикувахте това обяснение. Току-що прекарахме един час в неизправна спецификация, преди да открием това. :( - person LandonSchropp; 16.12.2016

Може би си мислите, че това е проблем, защото няма начин да има

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

и да има getSomeText "first" отпечатано преди "second", когато резолюцията е синхронна.

Но мисля, че имате логически проблем.

Ако вашата getSomeText функция може да е синхронна или асинхронна, в зависимост от контекста, тогава тя не трябва да влияе на реда на изпълнение. Използвате обещания, за да сте сигурни, че винаги е едно и също. Наличието на променлив ред на изпълнение вероятно ще се превърне в грешка във вашето приложение.

Използвайте

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

И в двата случая (синхронен с cast или асинхронна резолюция) ще имате правилния ред на изпълнение.

Обърнете внимание, че наличието на функция, която понякога е синхронна, а понякога не, не е странен или малко вероятен случай (помислете за обработка на кеша или групиране). Просто трябва да предположите, че е асинхронно и всичко винаги ще бъде наред.

Но искането на потребителя на API да прецизира с булев аргумент, ако иска операцията да бъде асинхронна, изглежда няма смисъл, ако не напуснете областта на JavaScript (т.е. ако не използвате някакъв собствен код ).

person Denys Séguret    schedule 16.01.2014
comment
Целта на кода беше да има пример, при който обещанието за синхронизиране ще доведе до първата секунда. Моят код трябва да показва само рамката на моя проблем, а не начина, по който ще използвам обещания. Вие сте прав с вашия втори код-пример, който обещава, че ще гарантира, че винаги ще доведе до планираната поръчка за печат. - person thuld; 17.01.2014
comment
Истински важен момент е, че незабавното изпълнение на код, зависещ от обещание, е грешка. Липсата на синхронно разрешаване на обещания ще ви помогне да избегнете този вид грешка. - person Denys Séguret; 17.01.2014
comment
Проблемът с .then (и обратното извикване, отложено до следващия цикъл на изпълнение) е, че добавя значително забавяне в повечето реализации, правейки Promises неприемливо решение за критични за времето процеси, като анимации. Например, ако имате нещо като: spinUp.then(rotateXtimes).then(slowDown), ще получите забележими прекъсвания на анимацията, докато синхронната резолюция с прости обратни извиквания като spinUp(function(){rotateXtimes(function(){slowDown(done)})}) не въвежда никакви забавяния. - person metalim; 31.05.2016
comment
@DenysSéguret Истинският важен момент е, че незабавното изпълнение на код, зависещ от обещание, е грешка. Не винаги е бъг. Има случаи на използване за синхронно разрешаване/изпълнение на обратни извиквания, когато данните вече са налични и ги искате синхронно, но искате да използвате повторно съществуващия кодов път с възможност за асинхронизиране. (например моят текущ случай на употреба тук: github.com/stacktracejs/stacktrace.js /issues/188) - person Venryx; 11.04.2017

Целта на обещанията е да направят асинхронния код по-лесен, т.е. по-близо до това, което усещате, когато използвате синхронен код.

Използвате синхронен код. Не го правете по-сложно.

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 изважда вашия код от нормалния поток на изпълнение, защото се предполага, че е асинхронен. Bluebird го прави по правилния начин. Просто обяснение какво прави:

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

Имайте предвид, че bluebird всъщност не прави това, само за да ви даде прост пример.

Опитай!

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

Това ще изведе следното:

second
first 
person Florian Margaine    schedule 16.01.2014
comment
+1, защото мисля, че сте разбрали правилно, но направих друг отговор, защото не съм сигурен, че отговорът ви е наистина лесен за разбиране от гледна точка на OP (може и да греша, разбира се). - person Denys Séguret; 16.01.2014
comment
@Florian Margaine Честно казано, предпочитам начина, по който jQuery обработва „тогава“ синхронно. Използвам го например, за да изградя динамична верига от събития, при която следващите функции разчитат на резултати от предишните, които също варират въз основа на определени критерии, и се обработват от споделена функция за отказ и извършено. Алтернативата без обещания обикновено води до сложна серия от функции за обратно извикване, които са много трудни за следване. Без да чукам bb тук, обмислям мигриране заради предимствата на производителността. Просто не разбирам как бих постигнал това иначе, ако then винаги е async. - person Brandon; 03.01.2015
comment
@Brandon За какво говориш? Ако имате синхронизиращи функции, не използвайте обещания - ако имате асинхронни функции, jQuery deferreds създава състезателни условия, като изпълнява асинхронно понякога от друга страна Bluebird винаги изпълнява по същия начин независимо от расата. - person Benjamin Gruenbaum; 03.01.2015
comment
Малко е трудно да се изрази без примерен код и не искам да отвличам нишката. По принцип множество зависими тогава функции срещу обещание, които се решават, след като веригата от функции е подредена. като myPromise.then(functionA).then(dynamicFunc).then(functionD).fail(failFunction).done(doneFunction), където dynamicFunc може да бъде функцияB или functionC. след това просто извикване на myPromise.resolve({data}), за да започне. Всяка функция дефинира отложено и връща обещание (с изключение на done и fail), след което разрешава или отхвърля отложеното или предава данни на следващата функция, или извикванията са неуспешни. - person Brandon; 06.01.2015
comment
Между другото, можеш ли да обясниш състоянието на състезанието? Може би с пример? Изглежда не мога да намеря много информация конкретно защо това може да се случи. Ако знаете някакви добри ресурси, ще се радвам да видя. - person Brandon; 06.01.2015
comment
@Brandon Добре, от една страна, ако приемете, че този дълъг низ от действия ще се изпълни синхронно (и сте действали според това предположение по някакъв начин), а след това част от него се окаже асинхронен, ще бъдете SOL. Всяка малка полза от производителността, която получавате от това да работи синхронно, е просто преждевременна оптимизация. Като странична бележка, не е необходимо да връщате ново обещание от манипулираща функция, която предавате на then(). Ако върнете обикновена стойност, тя ще бъде предадена на следващия манипулатор съвсем добре. - person JLRishe; 13.02.2015

Тук вече има някои добри отговори, но за да обобщим същината на въпроса много накратко:

Наличието на обещание (или друг асинхронен API), който понякога е асинхронен, а понякога синхронен, е лошо нещо.

Може да мислите, че е добре, защото първоначалното извикване на вашия API приема булево значение за превключване между синхронизиране/асинхронизиране. Но какво ще стане, ако това е заровено в някакъв обвиващ код и лицето, използващо този код, не знае за тези шенанигани? Току-що са завършили с някакво непредсказуемо поведение без вина по тяхна вина.

Изводът: Не се опитвайте да правите това. Ако искате синхронно поведение, не връщайте обещание.

С това ще ви оставя този цитат от Не познавате JS:

Друг проблем с доверието се нарича „твърде рано“. От гледна точка на конкретното приложение, това всъщност може да включва извикване, преди да бъде завършена някаква критична задача. Но по-общо, проблемът е очевиден в помощните програми, които могат или да извикат обратното извикване, което предоставяте сега (синхронно), или по-късно (асинхронно).

Този недетерминизъм около поведението на синхронизиране или асинхронизиране почти винаги ще доведе до много трудни за проследяване грешки. В някои кръгове измисленото предизвикващо лудост чудовище на име Залго се използва за описание на синхронизираните/асинхронни кошмари. — Не пускай Залго! е често срещан вик и води до много разумен съвет: винаги извиквайте обратните извиквания асинхронно, дори ако това е „веднага“ на следващия ход на цикъла на събитието, така че всички обратни извиквания да са предвидимо асинхронни.

Забележка: За повече информация относно Zalgo вижте "Don't Release Zalgo!" на Oren Golan. (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) и „Designing APIs for Asynchrony“ на Isaac Z. Schlueter (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“ всъщност е невероятно често срещан и солиден съвет. Винаги бъдете асинхронни.

person JLRishe    schedule 12.02.2015
comment
+1 за споменаването на проблемите с това несъответствие, щях да проследя сам статиите на Zalgo (; - person pospi; 19.10.2016

Какво ще кажете за този случай, също свързан с CrmFetchKit, който в последната версия използва Bluebird. Надстроих от версия 1.9, която беше базирана на jQuery. Все още старият код на приложението, който използва 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);" в новата версия, без да се налага да променяте прототипи в приложението, което използва тази библиотека?

person Nicolas    schedule 19.05.2016
comment
Обещанията могат да разрешават или отхвърлят само една стойност. Можете да разрешите целия обект result и след това да промените функцията за обратно извикване, предоставена на .then(), за да приема само обекта result като аргумент. - person idbehold; 19.05.2016
comment
Благодаря, изглежда това е единственият начин. Промених кода на приложението, за да приеме един обект като параметър и да извлече оттам другите два. Предполагам, че това е критична промяна при надграждане на библиотеката до нова версия. - person Nicolas; 19.05.2016

Всъщност можете да направите това, да.

Променете файла 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

person Venryx    schedule 11.04.2017
comment
Модифицирането на bluebird, за да се наруши съответствието със спецификациите на Promises/A+, е лоша идея. Помислете за човека, който по-късно ще трябва да отстранява грешки в кода, който сте написали... и естествено приема съответствие с Promises/A+. - person trincot; 20.05.2017