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

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

Таким образом, JavaScript в основном однопоточный.
Каждая операция выполняется одна за другой в этом основном потоке.

A. do this
B. do this
C. do this

Будет выполнено A
затем B
затем C

Последовательно. Здравый смысл, правда?

Но иногда это не так. Давайте посмотрим -

let name = "Heisenberg"

Эта переменная name имеет значение. Вы хотите распечатать это значение. Итак, вы пишете для этого программу.

console.log(name)

Что делать, если это значение / данные недоступны в вашем коде. Это где-то еще снаружи. Может быть, какой-то сервер обслуживает это значение, когда мы отправляем HTTP-запрос.
Или, может быть, оно находится внутри файла.

Так что сейчас этого нет в вашем коде. Ваша программа должна будет получить его извне.

Теперь код выглядит так -

let name;
// some imaginary Service
// which sends us a String value as response
fetch("/saymyname")
  .then( res => res.text() )
  .then( value => name = value )
console.log(name)

Ошибка в коде.
Результат будет -

undefined

name переменная все еще не определена, поскольку она была инициализирована. Это не было переопределено, как мы хотели сделать в коде выборки.

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

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

let name;
fetch("/saymyname")
  .then( res => res.text() )
  .then( value => {
     name = value
     console.log(name)
   })

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

Обычно JavaScript является синхронным. Но в языке есть некоторые специфические API, асинхронные по своей природе. Как и здесь, мы использовали fetch API. Это означает, что они будут пропущены и обработаны позже.

Это хорошо, потому что в противном случае эта программа зависла бы до тех пор, пока данные не станут для нас доступны. Но это также проблематично, потому что это не обычный способ написания кода, есть накладные расходы на синхронизацию асинхронных вещей. Для этого у нас есть гораздо более чистый API - Async / Await. Что также блокирует, но вы можете контролировать, где и когда вы хотите заблокировать.

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

Чтобы понять это, давайте посмотрим на другой пример -

Допустим, мы хотим прочитать текст из двух разных файлов. В Node есть асинхронный API для чтения файлов. Но теперь мы НЕ собираемся использовать настоящий API, а скорее какой-то псевдокод.

readFile("fileOne.txt") - say this might take 3 seconds to process
readFile("fileTwo.txt") - and this takes 5 seconds

Это самостоятельные операции. Итак, мы хотели бы - оба данных должны быть доступны через 5 секунд, а не 3 + 5 = 8 секунд. Итак, вот непараллельная / блокирующая версия.

Не параллельно - занимает 8 секунд

readFile("fileOne.txt")
  .then( text => {
     console.log("text from file one", text)
     
     readFile("fileTwo.txt")
       .then( text => console.log("text from file two", text) )
   })
console.log("Processing...")

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

Давайте попробуем использовать async / await.

Not Parallel - занимает 8 секунд.

async function readAsyncFiles() {
  
  let text1 = await readFile('/fileOne.txt')
  console.log("text from file one", text)
  let text2 = await readFile('/fileTwo.text')
  console.log("text from file one", text)
}
readAsyncFiles()
console.log("Processing...")

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

Так как это писать параллельно?

Параллельно - занимает 5 секунд

readFile("fileOne.txt")
  .then( text => console.log("text from file one", text) )
readFile("fileTwo.txt")
  .then( text => console.log("text from file two", text) )
console.log("Processing...")

Вывод будет в таком порядке -

Processing
text from file one <text>
text from file two <text>

Это круто.

Но что, если вы хотите напечатать «Готово», когда они оба закончат. Думаю об этом. Это немного сложно, не правда ли?

Один из вариантов - это то, как мы это уже писали, но не параллельно.

async function readAsyncFiles() {
  
  let text1 = await readFile('/fileOne.txt')
  console.log("text from file one", text)
  let text2 = await readFile('/fileTwo.text')
  console.log("text from file one", text)
  console.log("Done")
}
readAsyncFiles()

Другой вариант - печатать «Готово» внутри каждого блока «then». Тогда мы получим по два уведомления для каждого - и это нормально, но наша цель не в этом.

Вместо того, чтобы печатать «Готово», что, если бы мы хотели объединить текст из обоих файлов. Я имею в виду, что для этого нам пришлось написать хакерский код.

Вот почему у нас есть - Promise.all (). Этот API обрабатывает несколько независимых операций Async в параллельном режиме. И мы можем дождаться завершения всего процесса и в конце концов перейти к следующей строке.

Параллельно - 5 секунд.

// wrapped in async function
const [text1, text2] = await Promise.all([
                          readFile('/fileOne.txt'),
                          readFile('/fileTwo.txt')
                        ])
console.log("Done")

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

Кроме этого короткого замыкания API. Если какая-либо из этих операций завершится неудачно, с этого момента произойдет сбой всего. Что, если мы хотим, чтобы он работал как микросервис, то есть операция Async может завершиться ошибкой, но нам все равно нужны разрешенные значения других операций, тогда мы не сможем Promise.all (). Вместо этого нам нужно использовать Promise.allSettled ().

Итак, теперь у нас есть основная идея, что могут быть разные требования к асинхронным операциям и для их обработки, есть разные варианты Promise API. Как и еще один полезный инструмент, это Promise.race ().

Обещание может иметь 2 состояния. Ожидает и Решено / Отклонено.

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

На каждой итерации цикла событий мы можем рассмотреть 3 случая:

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

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

Давайте посмотрим на интересный случай -

setTimeout(()=> console.log('timeout'), 0)
Promise.resolve().then(()=> console.log('resolved promise'))
console.log('synchronous')

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

  • Если есть какой-либо обратный вызов разрешенного Promise to run.
  • Если нужно запустить обратный вызов таймера.

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

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

Итак, вывод кода -

synchronous
resolved promise
timeout

N.B. Различные браузеры могут иметь разные реализации. Это стандартное поведение Chrome / Node.

Чтобы понять, как на самом деле работает цикл событий, прочтите это- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/

И фантастическая статья Джейка Арчибальда о планировании микрозадач -
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

Это все, ребята. Удачи в асинхронном путешествии.