Синхронное разрешение промисов (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 не поддерживает разрешение в sync-режиме. Например, когда я делаю следующее, обработчик 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

Я ожидаю, что «второй» появится после «первого», потому что обещание уже разрешено со значением. Поэтому я бы предположил, что обработчик события then вызывается немедленно при применении к уже разрешенному объекту обещания.

Когда я сделаю то же самое (использую then для уже разрешенного обещания) с 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 работать таким же образом?

Обновление: предоставленный код предназначен только для иллюстрации проблемы. Идея библиотеки такова: независимо от режима выполнения (синхронный, асинхронный) вызывающая сторона всегда будет иметь дело с объектом обещания.

Что касается «... спрашивать пользователя ... не имеет смысла»: когда вы предоставляете два метода «CreateAsync» и «CreateSync», пользователь также должен решить, как выполняется операция.

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

Но я думаю, что понял, что метод синхронизации должен просто возвращать значение. В следующем выпуске (3.0) я реализую «CreateSync» и «CreateAsync».

Спасибо за ваш вклад.

Обновление-2. Мое намерение в отношении необязательного параметра состояло в том, чтобы обеспечить согласованное поведение и предотвратить логическую ошибку. Предположим, что вы являетесь потребителем моего метода «GetCurrentUserRoles», который использует lib. Таким образом, метод всегда будет возвращать обещание, а это означает, что вам нужно использовать метод «тогда» для выполнения кода, зависящего от результата. Поэтому, когда кто-то пишет такой код, я согласен, что это совершенно неправильно:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

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

    // ...
}

Я согласен, что этот код сломается, когда метод GetCurrentUserRoles изменится с синхронизации на асинхронность.

Но я понимаю, что это не очень хороший дизайн, потому что теперь потребитель должен иметь дело с асинхронным методом.


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 нет, это перегруженная функция (две разные функции), где вызывающая сторона выбирает синхронность. С таким же успехом API может быть CrmRestKit.CreateAsync и CrmRestKit.CreateSync вместо CrmRestKit.Create({async: ?}). Это полностью отличается от функции, которая иногда может знать свое возвращаемое значение синхронно (например, кеш 1-го уровня для вызова базы данных).   -  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 исключает необязательный параметр, который определяет, должен ли вызов веб-службы выполняться в синхронном или асинхронном режиме. Но если ОП просто бросит свой вопрос и не придет, чтобы просветить нас, мы можем просто потерять время...   -  person Denys Séguret    schedule 16.01.2014
comment
@dystroy, который не иногда возвращается асинхронно. Вызывающий выбирает, какой метод вызывать с помощью асинхронного параметра. Они либо вызывают синхронный метод, либо асинхронный метод. Это точно так же, как использование синхронного XHR — вы не будете использовать обратные вызовы, а только .responseText напрямую.   -  person Esailija    schedule 16.01.2014
comment
Ну, это быстро обострилось ... Идея в следующем: независимо от того, выполняется ли операция асинхронно или синхронно, у вас всегда должен быть один и тот же API -> вы получите обещание. @Esailija: Вы правы, это перегрузка функции. Пожалуйста, примите во внимание, что пример кода в вопросе - это всего лишь моя игровая площадка для понимания bluebird. Пожалуйста, взгляните на документацию моего проекта: crmrestkit.codeplex.com/documentation   -  person thuld    schedule 16.01.2014
comment
@thuld абсолютно нет, функции синхронизации должны возвращать прямые значения. Худший API, если вам приходится использовать промисы с синхронными функциями. То, что делает jQuery, очень неправильно, потому что это приведет к непредсказуемому порядку выполнения, когда функция действительно одновременно синхронна и асинхронна, то есть когда вызывающая сторона не может принять решение. Кроме того, это совсем не другой API для возврата значений из синхронных функций и обещаний из асинхронных. Посмотрите, например, на node fs API или XMLHttpRequest — ни один из них не заставляет вас использовать обратные вызовы или обещания при использовании синхронных вызовов.   -  person Esailija    schedule 16.01.2014
comment
Нет никакой разницы между наличием логического параметра и отдельными методами CreateAsync и CreateSync — пользователь выбирает и в случае логического параметра. Вот что я пытаюсь объяснить.   -  person Esailija    schedule 19.01.2014
comment
ОП, я думаю, у тебя достаточно информации, чтобы либо принять один из ответов, либо выбрать свой собственный, не так ли?   -  person Denys Séguret    schedule 05.02.2014
comment
@Esailija Независимо от того, хороший ли это дизайн (я думаю, вы правы, что это не так), могут быть случаи, когда запрос ОП приносит практическую пользу - когда есть существующая библиотека с глубокой цепочкой вызовов функций, основанных на обещаниях, и в некоторых случаях вы знаете, что все данные уже есть, и вы просто хотите получить их синхронно, чтобы вернуть их из функции или чего-то еще. В этом случае изменение системы 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 (и обратным вызовом, отложенным до следующего цикла выполнения) заключается в том, что он добавляет значительную задержку в большинстве реализаций, делая промисы неприемлемым решением для критичных по времени процессов, таких как анимация. Например, если у вас есть что-то вроде: spinUp.then(rotateXtimes).then(slowDown), вы получите заметные разрывы анимации, в то время как синхронное разрешение с простыми обратными вызовами, такими как spinUp(function(){rotateXtimes(function(){slowDown(done)})}), не вносит никаких задержек. - person metalim; 31.05.2016
comment
@DenysSéguret Действительно важным моментом является то, что наличие кода, зависящего от немедленного выполнения обещания, является ошибкой. Это не всегда ошибка. Существуют варианты использования синхронного разрешения/выполнения обратных вызовов, когда данные уже доступны, и вы хотите их синхронно, но хотите повторно использовать существующий путь кода с поддержкой асинхронности. (например, мой текущий вариант использования здесь: github.com/stacktracejs/stacktrace.js /вопросы/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, потому что я думаю, что вы поняли это правильно, но я дал другой ответ, потому что не уверен, что ваш ответ действительно легко понять с точки зрения ОП (конечно, я могу ошибаться). - person Denys Séguret; 16.01.2014
comment
@Florian Margaine Честно говоря, я предпочитаю, чтобы jQuery синхронно обрабатывал «затем». Я использую его, например, для создания динамической цепочки событий, в которой последующие функции полагаются на результаты предыдущих, которые также варьируются в зависимости от определенных критериев и обрабатываются общей функцией «сбой и выполнено». Альтернатива без промисов обычно приводит к запутанной серии функций обратного вызова, которым очень трудно следовать. Не сбивая bb здесь, я рассматриваю возможность миграции для повышения производительности. Я просто не понимаю, как бы я сделал это иначе, если then всегда асинхронно. - person Brandon; 03.01.2015
comment
@Брэндон О чем ты говоришь? Если у вас есть функции синхронизации, не используйте промисы. Если у вас есть асинхронные функции, jQuery откладывает создание условий гонки путем асинхронного запуска иногда, с другой стороны, Bluebird всегда запускает одинаково независимо от расы. - person Benjamin Gruenbaum; 03.01.2015
comment
Это немного сложно выразить без примера кода, и я не хочу захватывать тему. В основном несколько зависимых функций then против обещания, которые разрешаются после того, как цепочка функций организована. например, myPromise.then(functionA).then(dynamicFunc).then(functionD).fail(failFunction).done(doneFunction), где dynamicFunc может быть либо functionB, либо functionC. затем просто вызовите myPromise.resolve({data}) для запуска. Каждая функция определяет deferred и возвращает обещание (кроме done и fail), а затем разрешает или отклоняет deferred либо передает данные последующей функции, либо вызывает ошибку. - person Brandon; 06.01.2015
comment
Кстати, вы можете объяснить, что такое состояние гонки? Можно с примером? Я не могу найти много информации о том, почему это может произойти. Если вы знаете какие-либо хорошие ресурсы, я хотел бы видеть. - person Brandon; 06.01.2015
comment
@Brandon Ну, во-первых, если вы предположили, что длинная последовательность действий будет выполняться синхронно (и каким-то образом действовали в соответствии с этим предположением), а затем часть ее оказалась асинхронной, вы были бы SOL. Любой крошечный выигрыш в производительности, который вы получаете от синхронного запуска, является просто преждевременной оптимизацией. В качестве примечания: нет необходимости возвращать новое обещание из функции-обработчика, которую вы передаете then(). Если вы вернете обычное значение, оно будет передано следующему обработчику. - person JLRishe; 13.02.2015

Здесь уже есть несколько хороших ответов, но очень кратко подведем итог:

Промис (или другой асинхронный API), который иногда является асинхронным, а иногда синхронным, — это плохо.

Вы можете подумать, что это нормально, потому что первоначальный вызов вашего API принимает логическое значение для переключения между синхронизацией/асинхронностью. Но что, если это скрыто в каком-то коде-оболочке, и человек, использующий этот код, не знает об этих махинациях? Они только что столкнулись с каким-то непредсказуемым поведением не по своей вине.

Суть: Не пытайтесь это сделать. Если вам нужно синхронное поведение, не возвращайте обещание.

На этом я оставлю вам эту цитату из Вы не знаете JS:

Еще одна проблема доверия называется «слишком рано». С точки зрения конкретного приложения это может фактически включать вызов до завершения какой-либо критической задачи. Но в более общем плане проблема очевидна в утилитах, которые могут вызывать обратный вызов, который вы предоставляете сейчас (синхронно), или позже (асинхронно).

Этот недетерминизм в отношении синхронного или асинхронного поведения почти всегда приводит к тому, что очень трудно отследить ошибки. В некоторых кругах вымышленный вызывающий безумие монстр по имени Залго используется для описания синхронно-асинхронных кошмаров. «Не выпускайте Залго!» — распространенный крик, и он приводит к очень здравому совету: всегда вызывайте обратные вызовы асинхронно, даже если это «сразу же» на следующем этапе цикла событий, чтобы все обратные вызовы предсказуемо были асинхронными.

Примечание. Для получения дополнительной информации о Zalgo см. «Не выпускайте Zalgo!» Орена Голана. (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) и «Проектирование 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» на самом деле является невероятно распространенным и надежным советом. Всегда быть асинхронным.

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