Ответ 1
Комментарии
Во-первых, запустите promises внутри обработчика .then()
и НЕ возвращая эти promises из обратного вызова .then()
создает совершенно новую непривязанную последовательность обещаний, которая никак не синхронизируется с родительским promises, Обычно это ошибка, и на самом деле некоторые обещающие механизмы на самом деле предупреждают, когда вы это делаете, потому что это почти никогда не требует желаемого поведения. Единственный раз, когда вы когда-либо захотите это сделать, - это когда вы делаете какой-то огонь и забываете операцию, когда вас не волнуют ошибки, и вам не нужна синхронизация с остальным миром.
Итак, все ваши Promise.resolve()
promises внутри обработчиков .then()
создают новые цепочки Promise, которые выполняются независимо от родительской цепочки. У вас нет определенного поведения. Это похоже на запуск четырех аякс-вызовов параллельно. Вы не знаете, какой из них будет выполнен первым. Теперь, поскольку весь ваш код внутри этих обработчиков Promise.resolve()
оказывается синхронным (так как это не настоящий код мира), тогда вы можете получить согласованное поведение, но это не дизайнерская точка promises, поэтому я не буду " t потратить много времени, пытаясь выяснить, какая цепочка Promise, которая запускает синхронный код, только закончит сначала. В реальном мире это не имеет значения, потому что, если порядок имеет значение, тогда вы не оставите это случайно.
Резюме
-
Все обработчики
.then()
вызываются асинхронно после завершения текущего потока выполнения (как говорит спецификация Promises/A +, когда механизм JS возвращается к "коду платформы" ). Это справедливо даже для promises, которые разрешены синхронно, напримерPromise.resolve().then(...)
. Это делается для согласованности программирования, так что обработчик.then()
последовательно называется асинхронно независимо от того, разрешено ли обещание немедленно или позже. Это предотвращает некоторые ошибки синхронизации и упрощает для вызывающего кода постоянное асинхронное выполнение. -
Нет спецификации, которая определяет относительный порядок обработчиков
setTimeout()
по сравнению с запланированными.then()
, если обе они поставлены в очередь и готовы к запуску. В вашей реализации ожидающий обработчик.then()
всегда запускается до ожидающегоsetTimeout()
, но спецификация спецификации Promises/A + говорит, что это не определено. В нем говорится, что обработчикам.then()
можно назначить целый ряд способов, некоторые из которых будут выполняться перед ожидающими вызовамиsetTimeout()
, а некоторые из них могут запускаться после ожидающих вызововsetTimeout()
. Например, спецификация Promises/A + позволяет планировщикам.then()
планироваться либо с помощьюsetImmediate()
, которые будут выполняться перед ожидающими вызовамиsetTimeout()
, либо с помощьюsetTimeout()
, которые будут выполняться после ожидающих вызововsetTimeout()
. Таким образом, ваш код не должен зависеть от этого порядка. -
Несколько независимых цепочек Promise не имеют предсказуемого порядка выполнения, и вы не можете полагаться на какой-либо конкретный порядок. Это похоже на автоматическое выключение четырех аякс-вызовов, когда вы не знаете, какой из них будет выполнен первым.
-
Если порядок выполнения важен, не создавайте расы, зависящие от мельчайших деталей реализации. Вместо этого свяжите обеименные цепочки с целью принудительного выполнения определенного порядка выполнения.
-
Обычно вы не хотите создавать независимые цепочки обещаний в обработчике
.then()
, которые не возвращаются из обработчика. Обычно это ошибка, за исключением редких случаев пожара и забывания без обработки ошибок.
Линейный аналог
Итак, вот анализ вашего кода. Я добавил номера строк и очистил отступы, чтобы было легче обсуждать:
1 Promise.resolve('A').then(function (a) {
2 console.log(2, a);
3 return 'B';
4 }).then(function (a) {
5 Promise.resolve('C').then(function (a) {
6 console.log(7, a);
7 }).then(function (a) {
8 console.log(8, a);
9 });
10 console.log(3, a);
11 return a;
12 }).then(function (a) {
13 Promise.resolve('D').then(function (a) {
14 console.log(9, a);
15 }).then(function (a) {
16 console.log(10, a);
17 });
18 console.log(4, a);
19 }).then(function (a) {
20 console.log(5, a);
21 });
22
23 console.log(1);
24
25 setTimeout(function () {
26 console.log(6)
27 }, 0);
Строка 1 запускает цепочку обещаний и прикрепляет к ней обработчик .then()
. Поскольку Promise.resolve()
разрешается немедленно, библиотека Promise планирует запланировать первый обработчик .then()
, который будет запущен после завершения этого потока Javascript. В совместимых библиотеках обещаний Promises/A + все обработчики .then()
вызывается асинхронно после завершения текущего потока выполнения и когда JS возвращается к циклу событий. Это означает, что следующий другой синхронный код в этом потоке, например ваш console.log(1)
, будет следующим, который вы видите.
Все остальные .then()
обработчики на верхнем уровне (линии 4, 12, 19) цепочка после первого и будут запускаться только после того, как первый получит свою очередь. На данный момент они по существу поставлены в очередь.
Так как setTimeout()
также находится в этом начальном потоке выполнения, он запускается и, следовательно, запланирован таймер.
Это конец синхронного выполнения. Теперь, JS-движок запускает то, что запланировано в очереди событий.
Насколько я знаю, нет гарантии, которая на первом месте выполняет обработчик setTimeout(fn, 0)
или .then()
, оба из которых должны запускаться сразу после этого потока выполнения. Обработчики .then()
считаются "микро-задачами", поэтому меня не удивляет, что они запускаются раньше, чем setTimeout()
. Но если вам нужен конкретный заказ, тогда вы должны написать код, который гарантирует заказ, а не полагаться на эту деталь реализации.
В любом случае следующий .then()
обработчик, определенный в строке line 1. Таким образом, вы видите вывод 2 "A"
из этого console.log(2, a)
.
Далее, поскольку предыдущий обработчик .then()
возвращает равное значение, это обещание считается разрешенным, поэтому выполняется обработчик .then()
, определенный в строке line 4. Здесь, где вы создаете другую независимую цепочку обещаний и вводите поведение, которое обычно является ошибкой.
Строка 5, создает новую цепочку Promise. Он решает, что первоначальное обещание, а затем планирует два обработчика .then()
для запуска, когда выполняется текущий поток выполнения. В этом текущем потоке выполнения находится console.log(3, a)
в строке 10, поэтому вы видите следующее. Затем этот поток выполнения заканчивается, и он возвращается к планировщику, чтобы увидеть, что нужно делать дальше.
Теперь у нас есть несколько обработчиков .then()
в очереди, ожидающих следующего. Там тот, который мы только что запланировали в строке 5, а затем следующий в цепочке более высокого уровня в строке 12. Если вы сделали это на строке 5:
return Promise.resolve.then(...)
то вы бы связали эти promises вместе, и они будут скоординированы последовательно. Но, не вернув обещания, вы начали целую новую цепочку обещаний, которая не согласуется с внешним обещанием более высокого уровня. В вашем конкретном случае планировщик обещаний решает запустить более глубоко вложенный обработчик .then()
. Я не знаю, честно ли это по спецификации, по соглашению или только по одной детали реализации одного механизма обещания по сравнению с другим. Я бы сказал, что если заказ имеет для вас решающее значение, вы должны заставить порядок привязать promises в определенном порядке, а не полагаться на того, кто первым победит в гонке.
В любом случае, в вашем случае это расписание планирования и движок, который вы используете, решает запустить внутренний обработчик .then()
, который определен в строке 5 далее, и, таким образом, вы видите 7 "C"
, указанный в строке 6. Затем он ничего не возвращает, поэтому разрешенное значение этого обещания становится undefined
.
Вернувшись в планировщик, он запускает обработчик .then()
в строке 12. Это опять-таки гонка между этим обработчиком .then()
и тем, который находится на строке 7, которая также ждет выполнения. Я не знаю, почему он выбирает один за другим, кроме как сказать, что он может быть неопределенным или изменяться в зависимости от механизма обещания, потому что код не указан в коде. В любом случае запускает обработчик .then()
в строке 12. Это снова создает новую независимую или несинхронизированную цепочку цепочек обещаний предыдущей. Он снова запускает обработчик .then()
, а затем вы получаете 4 "B"
из синхронного кода в этом обработчике .then()
. Теперь все синхронный код выполняется в этом обработчике, он возвращается к планировщику для следующей задачи.
Вернувшись в планировщик, он решает запустить обработчик .then()
на строке 7, и вы получите 8 undefined
. Обещание есть undefined
, потому что предыдущий обработчик .then()
в этой цепочке ничего не возвращал, поэтому его возвращаемое значение было undefined
, таким образом, это разрешенное значение цепочки обещаний в этой точке.
В этот момент выход до сих пор:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
Опять же, весь синхронный код завершен, и он снова возвращается к планировщику и решает запустить обработчик .then()
, определенный в строке 13. Это выполняется, и вы получаете вывод 9 "D"
, а затем снова возвращается к планировщику.
В соответствии с ранее вложенной цепочкой Promise.resolve()
расписание выбирает запуск следующего внешнего обработчика .then()
, определенного в строке линии 19. Он запускается, и вы получаете вывод 5 undefined
. Он снова undefined
, потому что предыдущий обработчик .then()
в этой цепочке не возвращал значение, поэтому разрешенное значение обещания было undefined
.
В качестве этой точки выход до сих пор:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
В этот момент запланирован только один обработчик .then()
, поэтому он запускает один, определенный в строке line 15, и вы получите следующий результат 10 undefined
.
Затем, наконец, setTimeout()
запускается, а конечный вывод:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
Если бы кто-то попытался точно предсказать порядок, в котором это будет работать, тогда возникнут два основных вопроса.
-
Как ожидаются ожидающие запросы
.then()
обработчики с приоритетами по сравнению сsetTimeout()
, которые также ожидаются. -
Как механизм обещаний решает приоритезировать несколько обработчиков
.then()
, которые все ждут запуска. По вашим результатам с этим кодом это не FIFO.
Для первого вопроса, я не знаю, соответствует ли это спецификации или просто варианту реализации здесь в движке движков/JS-движке, но при реализации, о которой вы сообщили, приоритет приостанавливается для всех ожидающих обработчиков .then()
до того, как все setTimeout()
. Ваш случай немного странный, потому что у вас нет реальных вызовов API асинхронного программирования, кроме указания обработчиков .then()
. Если у вас была какая-либо асинхронная операция, которая фактически выполнялась в реальном времени для выполнения в начале этой цепочки обещаний, то ваш setTimeout()
выполнил бы перед обработчиком .then()
в реальной операции async только потому, что реальная операция async принимает фактическое время для выполнить. Итак, это немного надуманный пример и не является обычным примером для реального кода.
Во втором вопросе, я видел некоторое обсуждение, в котором обсуждается, как отложенные обработчики .then()
на разных уровнях вложения должны быть приоритетными. Я не знаю, разрешалось ли это обсуждение в спецификации или нет. Я предпочитаю кодировать таким образом, что этот уровень детализации не имеет значения для меня. Если меня волнует порядок моих асинхронных операций, я связываю свои цепочки обещаний, чтобы контролировать порядок, и этот уровень детализации реализации никоим образом не влияет на меня. Если меня не волнует порядок, тогда мне все равно, что этот порядок детализации не влияет на меня. Даже если это было в некоторых спецификациях, похоже, что тип деталей, которым не следует доверять во многих разных реализациях (разные браузеры, разные механизмы обещаний), если вы не тестировали его везде, где вы собираетесь запускать. Поэтому я бы рекомендовал не полагаться на определенный порядок выполнения, когда у вас есть несинхронизированные цепочки обещаний.
Вы можете сделать заказ на 100% определенным, просто связав все ваши цепочки обещаний, как это (возвращая внутреннее promises, чтобы они были связаны в родительскую цепочку):
Promise.resolve('A').then(function (a) {
console.log(2, a);
return 'B';
}).then(function (a) {
var p = Promise.resolve('C').then(function (a) {
console.log(7, a);
}).then(function (a) {
console.log(8, a);
});
console.log(3, a);
// return this promise to chain to the parent promise
return p;
}).then(function (a) {
var p = Promise.resolve('D').then(function (a) {
console.log(9, a);
}).then(function (a) {
console.log(10, a);
});
console.log(4, a);
// return this promise to chain to the parent promise
return p;
}).then(function (a) {
console.log(5, a);
});
console.log(1);
setTimeout(function () {
console.log(6)
}, 0);
Это дает следующий результат в Chrome:
1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6
И, поскольку обещание все соединено вместе, порядок обещаний определяется кодом. Единственное, что осталось в качестве детали реализации - это время setTimeout()
, которое, как в вашем примере, приходит последним, после всех обработчиков .then()
.
Edit:
При рассмотрении Promises/A + спецификации мы находим следующее:
2.2.4 onFulfilled или onRejected не должны вызываться, пока стек контекста выполнения не содержит только код платформы. [3,1].
....
3.1 Здесь "код платформы" означает код реализации, среды и обещания. На практике это требование гарантирует, что onFulfilled и onRejected выполняются асинхронно после события который затем вызывается, и с новым стекем. Это может быть реализован либо с помощью механизма макрозадачи, такого как setTimeout или setImmediate или с механизмом "микрозадания", таким как MutationObserver или process.nextTick. Поскольку реализация обещаний считается кодом платформы, он сам может содержать планирование задач очереди или "батуте", в котором вызываются обработчики.
Это говорит о том, что обработчики .then()
должны выполняться асинхронно после того, как стек вызовов вернется к коду платформы, но полностью исключает его реализацию, как именно это сделать, выполнив ли это макро-задачу, например setTimeout()
или микрозадание как process.nextTick()
. Таким образом, в соответствии с этой спецификацией она не определена и на нее нельзя полагаться.
Я не нахожу информацию о макрозаданиях, микро-задачах или сроках обещания .then()
обработчиков по отношению к setTimeout()
в спецификации ES6. Это, наверное, не удивительно, поскольку сам setTimeout()
не является частью спецификации ES6 (это функция среды хоста, а не функция языка).
Я не нашел никаких спецификаций, чтобы поддержать это, но ответы на этот вопрос Разница между микрозадачей и макрозадачей в контексте цикла событий объясняет, как все имеет тенденцию работать в браузерах с макрозаданиями и микро-задачами.
FYI, если вы хотите получить дополнительную информацию о микро-задачах и макро-задачах, здесь интересная справочная статья по теме: Задачи, микротовары, очереди и расписания.