Каков порядок выполнения обещаний в javascript

Я хотел бы объяснить себе порядок выполнения следующего фрагмента, который использует обещания javascript.

Promise.resolve('A')
  .then(function(a){console.log(2, a); return 'B';})
  .then(function(a){
     Promise.resolve('C')
       .then(function(a){console.log(7, a);})
       .then(function(a){console.log(8, a);});
     console.log(3, a);
     return a;})
  .then(function(a){
     Promise.resolve('D')
       .then(function(a){console.log(9, a);})
       .then(function(a){console.log(10, a);});
     console.log(4, a);})
  .then(function(a){
     console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);

результат:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

меня интересует порядок выполнения 1 2 3 7... не значения "А", "Б"...

Я понимаю, что если обещание разрешено, функция "тогда" помещается в очередь событий браузера. Так что мое ожидание 1 2 3 4 ...


@jfriend00 Спасибо, большое спасибо за подробную объяснения! Это действительно огромный объем работы!

2 ответов


комментарии

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

все Promise.resolve() обещания внутри .then() обработчики создают новые цепочки обещаний, которые работают независимо от родительской цепи. У вас нет определенного поведения. Это похоже на запуск четырех вызовов ajax параллельно. Вы не знаете, какой из них будет первым. Теперь, так как весь ваш код внутри этих Promise.resolve() обработчики происходит чтобы быть синхронным (поскольку это не код реального мира), вы можете получить согласованное поведение, но это не точка проектирования обещаний, поэтому я не стал бы тратить много времени на то, чтобы выяснить, какая цепочка обещаний, которая запускает синхронный код, будет завершена первой. В реальном мире это не имеет значения, потому что если порядок имеет значение, то вы не оставите все на волю случая.

резюме

  1. все .then() обработчики вызываются асинхронно после завершения текущего потока выполнения (как говорится в спецификации Promises/A+, когда JS engine возвращается к "коду платформы"). Это верно даже для обещаний, которые разрешаются синхронно, таких как Promise.resolve().then(...). Это делается для согласованности программирования, чтобы a .then() обработчик последовательно вызывается асинхронно независимо от того, разрешено ли обещание немедленно или позже. Это предотвращает некоторые ошибки синхронизации и облегчает вызов кода, чтобы увидеть согласованный асинхронное выполнение.

  2. нет спецификации, которая определяет относительный порядок setTimeout() и назначена .then() обработчики, если оба стоят в очереди и готовы к запуску. В вашей реализации ожидающий .then() обработчик всегда запускается перед ожидающим setTimeout(), но спецификация Promises/A+ spec говорит, что это не определено. Он говорит, что .then() обработчики могут быть запланированы целой кучей способов, некоторые из которых будут работать до ожидания setTimeout() звонки и некоторые из которых могут работать после до setTimeout() звонки. Например, Promises / A+ spec позволяет .then() обработчики, которые будут запланированы либо setImmediate() что бы добежать до до setTimeout() звонки или с setTimeout() который будет работать после ожидания setTimeout() звонки. Таким образом, ваш код вообще не должен зависеть от этого порядка.

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

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

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

Строка За Строкой Analsysis

Итак, вот анализ вашего кода. Я добавил номера строк и очистил отступ, чтобы было легче обсуждать:

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() немедленно разрешает, библиотека обещания запланирует первое .then() обработчик для запуска после завершения этого потока Javascript. В библиотеках Promises/A+, совместимых с promise, все .then() обработчики вызываются асинхронно после завершения текущего потока выполнения и когда JS возвращается в цикл событий. Это означает, что любой другой синхронный код в этом потоке, такой как ваш console.log(1) будет работать дальше, что вы видите.

все остальные .then() обработчики на верхнем уровне (строки 4, 12, 19 цепи) после первого и будет работать только после того, как первый получит свою очередь. На данный момент они стоят в очереди.

с setTimeout() также находится в этом начальном потоке выполнения, он запускается и, таким образом, таймер запланирован.

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

насколько я знаю, нет никакой гарантии, которая приходит первой setTimeout(fn, 0) или .then() обработчик, которые оба запланированы для запуска справа после этого поток выполнения. .then() обработчики считаются "микро-задачи", поэтому меня не удивляет, что они бегут перед setTimeout(). Но если вам нужен конкретный заказ, вы должны написать код, который гарантирует заказ, а не полагаться на эту деталь реализации.

все равно .then() обработчик определен на строка 1 проходит рядом. Таким образом, вы видите вывод 2 "A" от этого console.log(2, a).

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

строка 5, создает новую цепочку обещание. Он разрешает это первоначальное обещание, а затем планирует два .then() обработчики для запуска при выполнении текущего потока выполнения. В этом текущем потоке выполнения является console.log(3, a) на строка 10, поэтому вы видите следующее. Затем этот поток выполнения завершается, и он возвращается к планировщику, чтобы узнать, что запускать дальше.

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

return Promise.resolve.then(...)

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

в любом случае, в вашем случае это гонка планирования, и двигатель, который вы запускаете, решает запустить внутренний .then() обработчик, который определен в строке 5 далее, и, таким образом, вы видите 7 "C" указал на строка 6. Тогда он ничего не возвращает, поэтому разрешенное значение этого обещания становится undefined.

назад в планировщик, он запускает .then() обработчик на линия 12. Это снова гонка между тем .then() обработчик и один на строка 7 который также ждет запуска. Я не знаю, почему он выбирает один над другим здесь, кроме как сказать, что он может быть неопределенным или варьироваться в зависимости от механизма обещания, потому что заказ не указан кодом. В любом случае,.then() проводник в строка 12 начинает работать. Это снова создает новую независимую или несинхронизированную линию цепочки обещаний предыдущей. Он планирует .then() обработчик снова, а затем вы получите 4 "B" из синхронного кода в этом .then() обработчик. Весь синхронный код выполняется в этом обработчике, поэтому теперь он возвращается в планировщик для следующей задачи.

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

на данный момент выход до сих пор:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined

опять же, весь синхронный код выполняется, поэтому он снова возвращается к планировщику, и он решает запустить .then() обработчик определен на строка 13. Это работает, и вы получаете выход 9 "D" и затем он снова возвращается к планировщику.

в соответствии с ранее вложенными Promise.resolve() цепь, расписание выбирает для запуска следующий внешний .then() обработчик определен на строка 19. Он запускается, и вы получаете выход 5 undefined. Это снова undefined, потому что обработчик в этой цепочке не вернул значение, таким образом, разрешенное значение обещания было undefined.

как этот момент, выход до сих пор:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined

на данный момент существует только один .then() обработчик запланирован для запуска, поэтому он запускает тот, который определен на строка 15 и вы получаете выход 10 undefined далее.

затем, наконец,setTimeout() запускается, и конечный результат:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

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

  1. как идут дела .then() обработчики приоритеты и setTimeout() вызовы, которые также находятся в ожидании.

  2. как движок promise решает приоритизировать несколько .then() обработчики, все жду, чтобы убежать. По вашим результатам с этим кодом это не FIFO.

для первого вопроса я не знаю, является ли это спецификацией или просто выбором реализации здесь, в движке promise engine/JS, но реализация, о которой вы сообщили, похоже, определяет приоритет всех ожидающих .then() обработчики перед любым setTimeout() звонки. Ваш случай немного странный, потому что у вас нет реальных асинхронных вызовов API, кроме указания .then() обработчики. Если бы они у тебя были ... асинхронная операция, которая фактически заняла любое реальное время для выполнения в начале этой цепочки обещаний, а затем ваш setTimeout() будет выполняться до .then() обработчик реальной асинхронной операции только потому, что реальная асинхронная операция занимает фактическое время для выполнения. Таким образом, это немного надуманный пример и не является обычным случаем дизайна для реального кода.

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


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

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:

после рассмотрения обещания/а+ спецификация, мы находим:

2.2.4 onFulfilled или onRejected не должны вызываться до тех пор, пока стек контекста выполнения не будет содержать только код платформы. [3.1].

....

3.1 здесь "код платформы" означает двигатель, среду и код реализации обещания. На практике это требование гарантирует, что onFulfilled и onRejected выполняются асинхронно после события петля, в которой затем называется, и со свежей стопкой. Это может быть реализовано с помощью механизма "макро-задачи", такого как setTimeout или setImmediate, или с механизмом "микро-задачи" как Mutationobserver'А или процесса.nextTick. После внедрения обещают считается кодом платформы, он может сам содержать задачу-планирование очередь или" батут", в котором называются обработчики.

это говорит о том, что .then() обработчики должны выполняться асинхронно после возвращения стека вызовов к коду платформы, но оставляет его полностью реализации, как именно это сделать, независимо от того, сделано ли это с макрос-задачей, такой как setTimeout() или микро-задач, как process.nextTick(). Таким образом, в соответствии с этой спецификацией он не определен и не должен полагаться.

я не нахожу никакой информации о макро-задачах, микро-задачах или сроках обещания .then() обработчики по отношению к setTimeout() в спецификации ES6. Это, пожалуй, не удивительно, так как не является частью ЕС6 спецификация (это функция среды, а не языка).

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

FYI, если вы хотите больше информации о микро-задачах и макро-задачах, вот интересная справочная статья по теме:задачи, микротаски, очереди и расписания.


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

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

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