Почему обещания Q.js асинхронны после того, как они были разрешены?

Если у меня есть следующее:

var deferred = Q.defer();

deferred.resolve();

var a = deferred.promise.then(function() {
    console.log(1);    
});

console.log(2); 

...почему я вижу 2, а потом 1 в консоли?

Я понимаю, что этот вывод является правильным в соответствии со спецификацией Promises, в которой говорится о вызове функции на следующем тике (например, setTimeout()), даже если он уже разрешен, но я не понимаю, почему.

Я хотел бы иметь код, который синхронно вызывает then для серии обещаний, предполагая, что все обещания были разрешены.

Мой реальный вариант использования заключается в том, что я пытаюсь использовать реализацию Angular, $q, и я хочу, чтобы все обратные вызовы then выполнялись в одном и том же цикле $digest, чтобы я не получал ненужных последующих циклов $digest.


person ChrisBellew    schedule 22.07.2014    source источник
comment
Вы пытались использовать $q.all(), чтобы дождаться всех обещаний, а затем просто получить доступ к каждому значению в одной функции?   -  person Bergi    schedule 23.07.2014
comment
см. также Какова цель пункта 2.2.4 спецификации Promise/A+?   -  person Bergi    schedule 22.08.2016


Ответы (2)


Ответ - последовательность.

В реальном коде у вас нет промисов, которые всегда разрешаются сразу после создания, они были бы бессмысленны. Итак, у вас есть обещания, которые иногда могут быть немедленно решены.

В этом случае вам не нужен другой поток. Вам нужен всегда один и тот же предсказуемый поток. Итак, вы хотите, чтобы следующая функция всегда вызывалась на следующем такте.

Не используйте обещание, когда оно вам не нужно.

person Denys Séguret    schedule 22.07.2014
comment
Я считаю, что техническим термином является «Не выпускайте Zalgo», метод должен всегда быть либо асинхронным, либо синхронным, иначе вы получите действительно странные условия гонки, которые заставят вас плакать. - person Benjamin Gruenbaum; 22.07.2014
comment
Спасибо за Ваш ответ. Может быть, я не хочу обещаний. Мне нужен аккуратный способ запроса HTTP-ресурса, предоставления его первоначальному вызывающему абоненту, а затем кэширования его для предоставления последующим вызывающим абонентам. Я использовал обещание, которое выполняет HTTP-запрос, и я сохраняю объект обещания, чтобы последующие вызывающие абоненты могли передать обратный вызов «затем» и вызвать его как можно скорее, желательно до следующего тика (у меня есть другое поведение, которое называется на следующем тике, и я хочу, чтобы этот код был выполнен до того, как запустится другое поведение). Считаете ли вы, что мое использование обещаний неуместно? Спасибо еще раз. - person ChrisBellew; 22.07.2014
comment
Если у вас есть функция, которая иногда вызывает запрос, а иногда использует кеш, может быть уместно заставить ее возвращать обещание. Я делаю это иногда, и это достаточно быстро (но я использую более быструю библиотеку обещаний). - person Denys Séguret; 22.07.2014
comment
@TriParkinson Я бы не стал беспокоиться о том, что он будет медленнее, если у вас нет сотен обещаний. $q не всегда планирует цикл дайджеста. - person Benjamin Gruenbaum; 22.07.2014
comment
Стоит знать, что существуют реализации промисов, которые не применяют это правило и успешно используются в сложных выпускаемых приложениях. См., например. github.com/medikoo/deferred (я автор) - person Mariusz Nowak; 24.07.2014
comment
@MariuszNowak На мой взгляд, это очень плохая идея. Те очень немногие случаи, когда это может привести к повышению производительности, не компенсируют трудности анализа кода и незначительных ошибок, к которым это приведет. - person Denys Séguret; 24.07.2014
comment
@dystroy Когда вы понимаете, что это так работает, проблем не возникает. Пользуюсь много лет, не помню ни одной ошибки, также не было сообщений об ошибках от других пользователей. Это больше вопрос отношения. Вы должны быть открыты, чтобы попробовать такую ​​реализацию, ничто другое вас не убедит - person Mariusz Nowak; 24.07.2014
comment
@MariuszNowak, да ладно... 33 выпуска за несколько лет - это не так много пользователей... Я могу понять вашу позицию по поводу .done (который, я до сих пор считаю, является избыточным артефактом наивной реализации для выполнения чего-то, что обещает библиотека или родной код должен сделать) - но выпускать Zalgo? Выполнение такой отправки синхронизации — ужасная идея, и обещания jQuery печально известны из-за этого. Мне лично приходилось иметь дело с несколькими такими ситуациями с промисами jQuery (которые демонстрируют такое поведение)... это определенно не "не проблема". - person Benjamin Gruenbaum; 24.07.2014

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

Как только стандарт для JS выходит в Интернет, его можно отозвать, даже если он нарушен, поскольку идея состоит в том, что веб-страницы не должны ломаться. Если кто-то написал страницу сегодня, а затем умер, ее можно будет просмотреть в вашем браузере через пять лет. Было бы очень проблематично, если бы при просмотре веб-страниц вы продолжали натыкаться на страницы, которые не работают с вашим браузером.

В очень простых случаях использования это не приносит большого вреда и устраняет путаницу в отношении того, может ли что-то быть асинхронным.

Для все более нетривиальных вариантов использования это причиняет все больше вреда и добавляет путаницы. Поначалу кажется, что проще рассуждать о вашем коде, но он жертвует менее тривиальным использованием ради наименее тривиального использования.

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

Существует промежуточное решение, в котором можно запускать каждую задачу так, как будто она запускается после завершения текущего кода, но все запланировано в правильном порядке. Это не было реализовано, и это также спорно, поскольку компромиссы не всегда приводят к лучшим решениям. Этот компромисс приводит к проблемам с производительностью для стеков прямого возврата.

Способ работы promises означает, что стек прямых вызовов выполняется в глубину и выполняется до завершения (изолировано), а стек возвратных вызовов выполняется в ширину и выполняется по частям, чередующимся с другими стеками возвратных вызовов. Это два радикально разных понятия и поведения. Традиционно с обратными вызовами оба работают одинаково.

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

Вы не можете полагаться на порядок, что означает, что есть случаи с обещаниями, когда, если вы просите много вещей, когда получаете их обратно, вам нужно дважды проверить, что это то, о чем вы просили, где вам не нужно было бы делать это с обратные вызовы. Вам также может понадобиться буферизовать и изменить порядок событий, что вам не нужно делать с обратными вызовами. Это также может сделать тесты ненадежными, если вы не постараетесь изолировать две выполняемые вещи.

Это также может создать серьезные проблемы с производительностью, которые не всегда легко предотвратить. Если вы используете промисы с сотней возвращенных результатов, возвращаемых сразу из одного события в итератор, каждый из которых занимает одну секунду на обратный вызов, а их глубина разрешения промисов равна двум, все они будут разбиты пополам с первыми половинами, которые все выполняются, а затем вторые половинки. Это означает, что потребуется 50,5 секунд, прежде чем что-либо сможет завершиться, тогда как с обратными вызовами через 50 секунд половина из них уже будет завершена. Если результат задачи передается другой внешней службе для обработки, она оставляет эту службу бездействующей в течение 50 секунд, когда она могла бы обрабатывать ваши результаты. Это делает обещания ужасными, когда вам нужна как низкая задержка, так и высокая пропускная способность для сервисов, которые берут на себя нагрузку, демонстрирующую слабость дизайна.

Невозможность наивно заменить обратные вызовы промисами — одно из самых разрушительных последствий этой ошибки проектирования, которая также перенесена в async/await. Если вы хотите преобразовать библиотеку обратного вызова, вы не можете просто изменить синтаксис, вы должны тщательно изучить семантику в каждой точке.

Планов по исправлению этого нет. Вы можете создавать свои собственные обещания, а генераторы могут использоваться для обеспечения того же синтаксиса, что и async/await, но с тем же предсказуемым и высокопроизводительным поведением обратных вызовов. Однако у вас могут возникнуть проблемы с другими библиотеками, которые все еще полагаются на собственные промисы.

person jgmjgm    schedule 01.10.2019
comment
Проголосовали против, потому что а) я не согласен с содержанием и б) это звучит как разглагольствование, но не могли бы вы пояснить, что вы имеете в виду под не реализовано, что все запланировано в правильном порядке и Вы не можете полагаться на порядок? Может с примерами? - person Bergi; 02.10.2019
comment
Содержание основано на изучении спецификации ES и последующем ее тестировании. Это все реально к сожалению для этого. Вы поднимаете вопрос, хотя. Я не тестировал его с Q. Я просто предположил, что они соответствуют одному и тому же стандарту взаимодействия. - person jgmjgm; 02.10.2019
comment
Я тестировал с Q, и, к сожалению, он определенно делает то же самое. Хотя, поскольку это пользовательская библиотека, ее можно отключить или изменить самостоятельно для значительного повышения производительности. - person jgmjgm; 02.10.2019
comment
Моя точка зрения не о Q против нативных промисов, я хочу сказать, что ваш пост действительно неясен о том, с каким кодом у вас проблемы с производительностью/планированием. - person Bergi; 02.10.2019
comment
Я не просил помощи с моим кодом. Я ответил на вопрос, почему это так. Вы должны проверить часть производительности, которую я добавил. Чуть позже я предоставлю тестовый скрипт, чтобы показать это на практике. - person jgmjgm; 02.10.2019
comment
Нет. Ответ на вопрос: Он делает это, потому что так указано, и, возможно, Он был разработан таким образом, потому что…. В вашем посте, напротив, только говорится, что я категорически не согласен с тем, как он работает, и считаю его плохим дизайном (мягко говоря). - person Bergi; 02.10.2019
comment
Дело в том, что у меня проблемы с исполнением. Что вы подразумеваете под обещанием решить глубину два? И как промисы могли иметь время планирования около 50 секунд, если они использовались с правильным неблокирующим кодом для параллельных задач? - person Bergi; 02.10.2019
comment
Давайте продолжим обсуждение в чате. - person jgmjgm; 02.10.2019
comment
Это немного некрасиво (нужно украсить), но попробуйте. Нам действительно нужны хорошие обещания в JS и лучшие вещи для асинхронности, но большинство людей не знают, что вещи, которые сейчас выходят, в основном незавершенные. Подходит для основных случаев использования, но не для серьезного использования в сложных приложениях. - person jgmjgm; 02.10.2019
comment
Кажется, ваша основная проблема заключается в том, что вы пытаетесь использовать промисы с синхронным блокирующим кодом (эта функция wait), для чего они никогда не были разработаны. Вместо этого используйте асинхронный таймер, и они будут летать. Подробнее в чате. - person Bergi; 02.10.2019