Понимание async JS с обещаниями, задачами и очередью заданий

Я изучал асинхронное поведение в JS, и по большей части все шло хорошо. Я понимаю синхронный способ выполнения кода, единый поток JS и то, как обратные вызовы, такие как внутри setTimeout, будут синхронизироваться API веб-браузера, а затем добавлены в очередь задач.

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

Это довольно просто, и поэтому следующий код:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');

Выведет start, end, timeout.

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

В этом все еще есть смысл, и в этом можно убедиться, запустив:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

Это выводит start, end, promise, timeout. Выполняется синхронный код, обратный вызов then помещается в стек из очереди микрозадач и выполняется, задача обратного вызова setTimeout из очереди задач отправляется и выполняется. Пока все хорошо.

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

console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    resolve('promise 1');
});

p1.then(msg => console.log(msg));

console.log('end');

Приведенный выше фрагмент выведет start, promise 1 log, end, promise 1, доказывающий, что исполнитель работает синхронно.

И здесь меня путают с обещаниями, допустим, у нас есть следующий код:

console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    setTimeout(() => {
        resolve('promise 1');
    }, 0);
});

p1.then(msg => console.log(msg));

console.log('end');

Это приведет к start, promise 1 log, end, promise 1. Если функция-исполнитель запускается сразу, это означает, что setTimeout внутри нее будет помещен в очередь задач для последующего выполнения. Насколько я понимаю, это означает, что обещание еще не выполнено. Мы переходим к методу then и обратному вызову внутри него. Это будет помещено в очередь заданий. остальная часть синхронного кода выполняется, и теперь у нас есть пустой стек вызовов.

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

Это оказалось длинным, так что я благодарен всем, кто нашел время, чтобы прочитать и ответить на это. Я хотел бы лучше понять очередь задач, очередь заданий и цикл событий, поэтому не стесняйтесь публиковать подробный ответ! Заранее спасибо.


person Konstantinos Pascal    schedule 05.08.2020    source источник
comment
И имейте в виду, что в большинстве случаев этот относительный приоритет различных видов асинхронных событий НЕ является чем-то, на что вы должны полагаться при кодировании, потому что все это в любом случае асинхронные гонки с непредсказуемым временем. Если вы хотите, чтобы конкретный ответ обрабатывался перед другим ответом, вы должны написать свой код, чтобы он действительно делал это, не учитывая этот уровень временных мелочей, заставляя определенную последовательность в том, как вы пишете свой код.   -  person jfriend00    schedule 05.08.2020
comment
В этом есть смысл, спасибо за напоминание. Я все еще новичок в этих асинхронных концепциях и хочу их хорошо понимать. Я могу только представить, что при работе с данными или функциями, которые могут занять непредсказуемое количество времени, лучше просто принудительно упорядочить их так, как вы хотите, и верным способом.   -  person Konstantinos Pascal    schedule 05.08.2020


Ответы (2)


... обратный вызов обещания теперь будет иметь приоритет ...

Задачи в очереди микрозадач получают приоритет над задачами в очереди задач только в том случае, если они существуют.

В примере:

  • Никакая микрозадача не ставится в очередь до тех пор, пока задача setTimout() не решит обещание.
  • Задача и микрозадача не конкурируют. Они последовательные.
  • Задержки, вызванные очередью задач и очередью микрозадач (в указанном порядке), складываются.

... но как это может быть выполнено с невыполненным обещанием?

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

person Roamer-1888    schedule 05.08.2020
comment
С пустым стеком вызовов и задачами как в очереди задач, так и в очереди заданий, цикл событий сначала заберет функцию из очереди заданий, чтобы передать ее в стек вызовов, не так ли? Вот что я имел в виду под приоритетом. Но, как вы сказали, then () зависит от задачи, помещенной в очередь задач. Будет ли этот факт заставлять цикл событий идти и сначала выталкивать эту задачу из очереди задач, прежде чем вернуться к тогдашнему обратному вызову, который помещается в очередь заданий? - person Konstantinos Pascal; 05.08.2020
comment
Похоже, ваше недоразумение состоит в том, что promise.then(callback) помещает задачу в очередь микрозадач. Это не так. Скорее, он удерживает callback в состоянии ожидания до тех пор, пока promise не будет выполнен. Только после этого он сделает запись в очереди микрозадач. - person Roamer-1888; 05.08.2020
comment
Ооо, это имеет большой смысл. Добавив дополнительный журнал внутри тайм-аута, я увидел, что тайм-аут cb действительно был запущен до моего тогда cb, и, как вы сказали, это было бы потому, что не было микрозадач, которые нужно было поднять. Затем я предположил, что функция разрешения поместит микрозадачу, но ваш ответ прояснил это. - person Konstantinos Pascal; 05.08.2020
comment
Ух! Я думал, меня ждет затяжной :-) - person Roamer-1888; 05.08.2020
comment
Да, теперь я понял. Мне было бы интересно узнать, как обратный вызов внутри затем приостанавливается до тех пор, пока обещание не будет выполнено, немного более подробно, если вы не возражаете! - person Konstantinos Pascal; 05.08.2020
comment
@KonstantinosPascal - обещание хранит внутри массив слушателей для уведомлений о разрешении и отклонении. Соответствующие слушатели (обратные вызовы) затем вставляются в очередь задач тогда и только тогда, когда обещание становится разрешенным или отклоненным, и, следовательно, эти слушатели должны быть вызваны. Это очень похоже на прослушиватели событий на EventEmitter, за исключением того, что эти прослушиватели могут быть вызваны только один раз (согласно спецификациям дизайна Promise). - person jfriend00; 05.08.2020
comment
Я не могу ничего добавить к объяснению @ jfriend00. - person Roamer-1888; 05.08.2020

Мы переходим к методу then и обратному вызову внутри него. Это будет помещено в очередь заданий.

Нет, при вызове then ничего не помещается в очередь заданий немедленно, если обещание еще не выполнено. Обратный вызов будет установлен на обещание для выполнения позже, когда обещание будет выполнено, точно так же, как обработчик событий. Только когда вы вызываете resolve(), он фактически помещает его в очередь заданий.

Это работает так же, как setTimeout, где вы написали [] обратный вызов […] будет рассчитан API веб-браузера, а позже добавлен в очередь задач - он не сразу ставит в очередь задачу, которая каким-то образом ожидает, но ожидает и затем ставит задачу в очередь для выполнения обратного вызова.

person Bergi    schedule 05.08.2020