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

Мое первоначальное решение: спойлер… не сработало

Если я хочу создать строку со значениями, извлеченными из нескольких запросов, все, что мне нужно сделать, это создать цикл forEach, верно?

async function createEmailBody(listOfUrls: string[]) {
  // Variable that I will append the data to
  let stringFromRequests = "";
 
  // A forEach loop to make each request and append to the master string
  listOfUrls.forEach(async (url) => {
    const urlResponse = await getResFromUrl(url); // This returns string value
    stringFromRequests = stringFromRequests + JSON.stringify(urlResponse.data);
  })

  return stringFromRequests;
}

// Now we access the emailBody string
async function populateEmail() {
  const emailBody = await createEmailBody();
  console.log(emailBody); // Prints: "" (empty string)
}

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

Что-то пошло не так.

async function createEmailBody(listOfUrls: string[]) {
  let stringFromRequests = "";

  listOfUrls.forEach(async (url) => {
    const urlResponse = await getResFromUrl(url);
    stringFromRequests = stringFromRequests + JSON.stringify(urlResponse.data);
  })

  return stringFromRequests; // stringFromRequests === "" (empty string)
}

Предательство цикла forEach

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

// forEach is ignoring the await !!!
listOfUrls.forEach(async (url) => {
  const urlResponse = await getResFromUrl(url);
  stringFromRequests = stringFromRequests + JSON.stringify(urlResponse.data);
})

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

Решение: Альтернатива forEach

Решение 1: for … цикла

Чтобы обойти странное поведение forEach, мы можем использовать цикл for… of в машинописном тексте, например так:

async function createEmailBody(listOfUrls: string[]) {
  // Variable that I will append the data to
  let stringFromRequests = "";
 
  // A for...of loop to make each request and append to the master string
  for (const url of listOfUrls) {
    const urlResponse = await getResFromUrl(url); // This returns string value
    stringFromRequests = stringFromRequests + JSON.stringify(urlResponse.data);
  }

  return stringFromRequests; // stringFromRequests === "data from urls" 😄
}

// Now we access the emailBody string
async function populateEmail() {
  const emailBody = await createEmailBody();
  console.log(emailBody); // Prints: "data from urls" 🎉
}

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

Решение 2: Promise.all() — что я в итоге использовал

Мы можем использовать Promise.all() для параллельной обработки всех запросов наших URL-адресов, вместо того, чтобы ждать один за другим, поскольку меня не волнует порядок, в котором это распечатывается.

async function createEmailBody(listOfUrls: string[]) {
  // Variable that I will append the data to
  let stringFromRequests = "";

  // Promise.all([urls]) runs every url request in parallel
  await Promise.all(listOfUrls.map(async (url) => {
    const urlResponse = await getResFromUrl(url); // This returns string value
    stringFromRequests = stringFromRequests + JSON.stringify(urlResponse.data);
  }));

  return stringFromRequests; // stringFromRequests === "data from urls" 😄
}

// Now we access the emailBody string
async function populateEmail() {
  const emailBody = await createEmailBody();
  console.log(emailBody); // Prints: "data from urls" 🎉
}

Примечание: в этом решении Promise.all() принимает список в качестве параметра, поэтому мы вызываем .map, чтобы вернуть список асинхронных запросов для разрешения

Вывод: 1 000 000 способов решить проблему

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