Javascript обещает любопытство

96

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

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

выход

node promises.js
response: true
error: false
Густаво Алвес
источник
34
Никогда не следует полагаться на тайминги между независимыми цепочками обещаний.
Берги

Ответы:

136

Это классный вопрос, чтобы разобраться в нем.

Когда вы это сделаете:

verifier(3,4).then(...)

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

verifier(5,4).then(...)

шанс запустить его .then()обработчик перед предыдущей строкой, .catch()потому что он уже был в очереди до того, как .catch()обработчик из первой попадет в очередь, а элементы запускаются из очереди в порядке FIFO.


Обратите внимание, что если вы используете .then(f1, f2)форму вместо .then().catch(), она запускается тогда, когда вы этого ожидаете, потому что нет дополнительных обещаний и, следовательно, дополнительных тиков:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

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


Спецификация ES6 для заказа обратного вызова обещаний и более подробное объяснение

Спецификация ES6 сообщает нам, что «задания» обещаний (поскольку они вызывают обратный вызов из .then()или .catch()) выполняются в порядке FIFO в зависимости от того, когда они вставлены в очередь заданий. Он конкретно не называет FIFO, но указывает, что новые задания вставляются в конец очереди, а задания запускаются с начала очереди. Это реализует порядок FIFO.

PerformPromiseThen (который выполняет обратный вызов из .then()) приведет к EnqueueJob, что является тем, как запланировано фактическое выполнение обработчика разрешения или отклонения. EnqueueJob указывает, что ожидающее задание добавляется в конец очереди заданий. Затем операция NextJob извлекает элемент из начала очереди. Это обеспечивает порядок FIFO при обслуживании заданий из очереди заданий Promise.

Итак, в примере в исходном вопросе мы получаем обратные вызовы для verifier(3,4)обещания и verifier(5,4)обещания, вставленного в очередь заданий в том порядке, в котором они были запущены, потому что оба этих исходных обещания выполнены. Затем, когда интерпретатор возвращается в цикл обработки событий, он сначала берет verifier(3,4)задание. Это обещание отклонено, и в verifier(3,4).then(...). Таким образом, он отклоняет обещание, которое verifier(3,4).then(...)вернулось, и в результате verifier(3,4).then(...).catch(...)обработчик вставляется в jobQueue.

Затем он возвращается в цикл обработки событий, и следующее задание, которое он извлекает из jobQueue, - это verifier(5, 4)задание. У него есть обработанное обещание и обработчик разрешения, поэтому он вызывает этот обработчик. Это приводит response (5,4):к отображению вывода.

Затем он возвращается в цикл обработки событий, и следующее задание, которое он извлекает из jobQueue, - это verifier(3,4).then(...).catch(...)задание, в котором оно выполняется, и это приводит error (3,4)к отображению вывода.

Это потому, что .catch()в 1-й цепочке на один уровень обещания глубже, чем .then()во 2-й цепочке, которая вызывает упорядочивание, о котором вы сообщили. И это потому, что цепочки обещаний переходят с одного уровня на другой через очередь заданий в порядке FIFO, а не синхронно.


Общие рекомендации относительно использования этого уровня детализации планирования

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

jfriend00
источник
Чтобы быть более конкретным, это точное поведение нигде не задокументировано в спецификации обещаний, что делает его деталью реализации. У вас может быть разное поведение между интерпретаторами (например, Node.js, Edge и Firefox) или между версиями интерпретаторов (например, Node 12 против Node 14). В спецификации просто говорится, что обещания обрабатываются асинхронно, чтобы избежать кода zalgo (который, IMHO, был ошибочным, кстати, потому что он был мотивирован людьми, которые задавали подобные вопросы, желая зависеть от времени потенциально асинхронного кода)
slebetman
@slebetman - Разве не задокументировано, что обратные вызовы обещаний из отдельных обещаний называются FIFO в зависимости от того, когда они были вставлены в очередь, и не могут выполняться до следующего тика? Кажется, что упорядочение FIFO - это все, что здесь требуется, потому что .then()оно должно возвращать новое обещание, которое само должно асинхронно разрешать / отклонять в будущем тике, что и приводит к этому упорядочиванию. Вы знаете какую-либо реализацию, в которой не используется упорядочение конкурирующих обратных вызовов FIFO?
jfriend00
3
@slebetman Promises / A + не указывает этого. ES6 указывает это. (Однако ES11 изменил поведение await).
Берги
Из спецификации ES6 о порядке очереди. PerformPromiseThenприведет к тому, EnqueueJobкак будет запланирован вызов обработчика разрешения или отклонения. EnqueueJob указывает, что ожидающее задание добавляется в конец очереди заданий. Затем операция NextJob извлекает элемент из начала очереди. Это обеспечивает порядок FIFO в очереди заданий Promise.
jfriend00
@Bergi Что это за изменение awaitв ES11? Достаточно ссылки. Благодарность!!
Педро А.
49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

Вместо вывода a1, a2, a3, b1, b2, b3 вы увидите a1, b1, a2, b2, a3, b3 по той же причине - каждый затем возвращает обещание и переходит в конец цикла событий очередь. Итак, мы видим эту «гонку обещаний». То же самое и с вложенными обещаниями.

Таруками
источник