JavaScript | Асинхронный цикл

JavaScript | Асинхронный цикл

Асинхронный цикл — это то место, где можно попасть в ситуацию замедления работы приложения на JavaScript. Неправильное использование языковых конструкций async await может увеличить время ответа приложения, при «ждущих» запросах в базу данных, например. В результате пользователь долго будет ждать свой контент и возможно вы потеряете его, вместе с его деньгами. В общем «время — деньги». В IT это правило более применимо, чем в каком бы то ни было бизнесе.

Давайте отойдём от замудрённого названия «асинхронный цикл» и будем просто иметь в виду такой цикл, в котором нам нужно ждать выполнения функции на каждой итерации. Ключевое слово здесь «ждать». Любые походы на сторонние ресурсы или в базы требуют времени. Фактически мы имеем дело с узлами, которые общаются друг с другом в стиле «запрос/ответ».

Важно понимать различия между «текущей средой выполнения кода», в которой варится наш JavaScript и «средой выполнения другого языка». Каждая из этих сред имеет свой доступ к процессору и оперативной памяти. Так вот, если у нас нет каких-то данных в момент вызова скриптового файла в среде выполнения JavaScript кода, тогда нужно «ждать» этих данных от какой-то другой среды. Вот эта история и называется асинхронностью. Мы не можем прогнать весь алгоритм с максимальной скоростью работы процессора, потому что у нас не всё есть.

 

Две ситуации с применением или неприменением ключевых слов async await в цикле for.

  1. На каждой итерации цикла мы тормозим при ожидании [[PromiseResult]] из нашего объекта Promise
  2. На каждой итерации цикла мы максимально быстры — мы не ждём [[PromiseResult]] сразу, а доверяем это дело Promise.all()

 

Какова наша задача?

Мы хотим БЫСТРО получить из базы однотипные объекты, которые не зависят друг от друга. То есть, очерёдность их получения нам не важна!

 

Цикл for с await для получения [[PromiseResult]] на каждой итерации в JavaScript

function pr_obj(id){
   return new Promise((resolve, reject)=>{
      let sec = Math.ceil(Math.random()*3*1000);
      setTimeout(resolve, sec, {id:id, sec:sec});
   })
}

async function get_arr(){
   let arr = [];
   for(let x = 0; x < 10; x++){
      arr.push(await pr_obj(x))
   }
   return arr
}

await get_arr();

Что тут происходит? Мы объявляем две функции:

  1. pr_obj
  2. get_arr

Функция «pr_obj» обычная и она возвращает объект обещания Promise. Причём мы специально создаём такое обещание, которое всегда разрешается — не отклоняется. Эта функция принимает число, которое представляет собой идентификатор объекта, который мы хотим возвращать в [[PromiseResult]]. То есть результатом выполнения данного обещания будет обычный объект с ключами «id» и «sec».

Для имитации работы в реальном мире мы используем функцию задержки вызова нашего «разрешателя». Задержка эта генерируется случайным образом. Результат задержки может быть от 0 до 3 секунд.

Функция «get_arr» имеет приставку async, потому что внутри неё должны выполняться получения [[PromiseResult]] после ожиданий. Так среда выполнения кода будет задавать особое поведение при обработке этой функции, если внутри неё встретит ключевое слово await.

После объявлений функций мы вызываем нашу асинхронную функцию также с ключевым словом await. Почему? Потому что мы хотим дождаться её выполнения.

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

Давайте смотреть на результат вызова функции «get_arr»

 

Цикл for который ждёт PromiseResult из обещания при помощи await на каждой итерации - JavaScript
Цикл for который ждёт PromiseResult из обещания при помощи await на каждой итерации — JavaScript

Если представить, что мы 10 раз сходили в базу данных, что на каждый запрос в базу мы ждали ответа, то в этом примере мы прождали суммарно минимум 16 секунд. И это печально. Это очень долго. Это пример неправильного применения ожидания для независимых данных.

Например если мы хотим получить десять объектов автомобилей из базы с годом выпуска 2023. Нам всё равно в какой последовательности они придут. Тут главное отправить быстро много запросов в базу и получать ответы по мере поступления. Когда самый долгий запрос ответит, тогда можно все выводит в результат обещания.

Обещания выполняются? Да выполняются!

Все обещания выполняются? Да, все были выполнены, потому что мы получили массив ровно из 10 объектов.

Объекты разрешения правильные? Да, у всех есть два ключа со значениями.

В каком случае этот подход имеет место быть? Только в том случае, если данные зависят друг от друга и их очерёдность получения важна. Только так. Например, вы не можете посчитать общую площадь стен помещения, если у вас нет высоты потолка. Сначала у вас должна быть ширина, потом длина, потом высота и вот только тогда вы сможете вычислить сумму.

Так как же нам решить первоначальную задачу?

 

Цикл for создающий объекты обещаний Promise на каждой итерации в JavaScript

Нашу задачу можно решить без использования ключевого слова await в теле цикла. Мы даже обе функции можем сделать обычными, без приставки async.

function pr_obj(id){
   return new Promise((resolve, reject)=>{
      let sec = Math.ceil(Math.random()*3*1000);
      setTimeout(resolve, sec, {id:id, sec:sec})
   })
}

function get_arr(){
   let arr = [];
   for(let x = 0; x < 10; x++){
      arr.push(pr_obj(x))
   }
   return Promise.all(arr);
}

await get_arr();

Обе функции будут возвращать нам новые обещания. Обе функции не используют ключевые слова async и await.

На этот раз в цикле мы просто создаём новые объекты обещаний под каждый будущий «объект разрешения» для [[PromiseResult]]. Эти новые объекты обещаний мы складываем в один массив в теле цикла и возвращаем весь массив с обещаниями. Мы создаём их без задержек — на максимальной скорости.

Функция «get_arr» использует метод all() конструктора Promise. В метод all() мы передаём массив из обещаний, которые уже начали свои выполнения в параллельном режиме. Теперь гонка началась.

Эта функция (метод all()) возвращает новое обещание, которое выполняется с массивом значений выполнения для переданных обещаний, или отклоняет с причиной отклонения первого переданного обещания. Метод all() разрешает все элементы переданного итерируемого объекта в обещания при выполнении этого алгоритма.

В итоге нам останется только дождаться выполнения функции «get_arr» — то есть выполнения обещания из Promise.all, потому что выражение «get_arr()» вернёт нам обещание.

Асинхронный цикл - быстрый - на обещаниях Promise без async await - JavaScript
Асинхронный цикл — быстрый — на обещаниях Promise без async await — JavaScript