DevGang
Авторизоваться

Асинхронное программирование: как управлять несколькими обещаниями одновременно с Promise.all()

Асинхронное программирование на JavaScript позволяет выполнять ресурсоемкие задачи в фоновом режиме, не блокируя основной поток выполнения. Такие задачи, как обращения к API и обработка файлов, должны выполняться асинхронно. 

Promise.all() — это мощная функция, которая позволяет эффективно управлять несколькими асинхронными операциями одновременно. В этой статье мы подробно рассмотрим, как использовать Promise.all() для управления множеством Promise.

Погружаемся в детали!

Понимание Promise

Promise — это объект, который представляет возможный сбой или завершение асинхронного события. Давайте рассмотрим пример простого обещания.

const userId = 1;
let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (userId === 1) {
      resolve({ name: "John Doe", email: "john@example.com" });
    } else {
      reject(new Error("User not found"));
    }
  }, 1000);
});

Promise принимает функцию с двумя параметрами: resolve и reject. В нашем примере Promise будет выполнен, если операция завершится успешно (например, если userId === 1). В случае неудачи, Promise будет отклонен.

Обещание начинает свой жизненный цикл в состоянии ожидания и в конечном итоге переходит в состояние выполнено или отклонено. В данный момент Promise находится в состоянии рассмотрения.

Для работы с Promise мы используем метод .then(), чтобы обработать его результат.

В результате выводится пользовательские данные (если они выполнены), либо сообщение об ошибке (если отклонено). 

promise
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

Поскольку операция прошла успешно, обещание будет выполнено.

const userId = 1;
let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (userId === 1) {
      resolve({ name: "John Doe", email: "john@example.com" });
    } else {
      reject(new Error("User not found"));
    }
  }, 1000);
});

promise
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  });

Если мы изменим значение userId, Promise будет отклонен, и вы получите сообщение об ошибке User not found

Теперь представим, что у вас несколько Promise. В этом случае вы можете обрабатывать каждое Promise независимо друг от друга следующим образом:

const promise1 = new Promise((resolve, reject) => resolve(1));
const promise2 = new Promise((resolve, reject) => resolve(2));
const promise3 = new Promise((resolve, reject) => resolve(3));
promise1
  .then((value) => {
    console.log(value);
    promise2
      .then((value) => {
        console.log(value);
        promise3
          .then((value) => {
            console.log(value);
          })
          .catch((err) => {
            console.log("promise3 error", err);
          });
      })
      .catch((err) => {
        console.log("promise2 error", err);
      });
  })
  .catch((err) => {
    console.log("promise1 error", err);
  });

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

  • Замедление выполнения: Каждый Promise запускается только после завершения предыдущего. Это означает, что promise2 запускается после разрешения promise1, а promise3 — после разрешения promise2, что приводит к замедлению выполнения кода.
  • Ад обратных вызовов: Вложенная структура в цепочке .then() создает сложный «ад обратных вызовов», который затрудняет чтение и поддержку кода.
  • Сложная обработка ошибок: Каждая ошибка обрабатывается независимо, что усложняет управление ошибками.

Для решения этих проблем предлагается использовать Promise.all(), который позволяет запускать Promise одновременно. Это повышает производительность и упрощает обработку ошибок.

Использование асинхронных операций с помощью Promise.all()

Promise.all() принимает итерируемый набор обещаний и возвращает одно обещание. Синтаксис выглядит следующим образом:

Promise.all(iterable)

Если мы используем Promise.all() на примере, то наш код будет выглядеть следующим образом:

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values);
  })
  .catch((err) => {
    console.log("promise all error", err);
  });

Как вы можете видеть, такой подход более понятен и эффективен. 

Но как работает Promise.all(), если JavaScript — однопоточный язык, где каждый фрагмент кода ожидает завершения предыдущего?

Ключ в том, что Promise.all() работает на основе принципа параллелизма. Это означает, что все Promise в массиве начинают выполняться практически одновременно, без ожидания завершения одного перед началом другого.

Promise.all() разрешается только тогда, когда все Promise в массиве успешно выполняются. Если хотя бы одно Promise отклоняется, Promise.all() немедленно отклоняется, игнорируя результаты остальных Promise.

Promise.all() в действии: практические примеры

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

Рассмотрим несколько примеров, где Promise.all() может быть использована для повышения эффективности в реальных приложениях:

Получение данных из нескольких API

Представьте, что вы работаете над приложением, которое одновременно получает данные из двух разных API.

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

const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
const apiUrl2 = "https://jsonplaceholder.typicode.com/todos/2";
const fetchData = async () => {
  console.time("fetch");
  try {
    const response = await fetch(apiUrl);
    const data1 = await response.json();
    const response2 = await fetch(apiUrl2);
    const data2 = await response2.json();
    console.log(data1);
    console.log(data2);
    console.timeEnd("fetch");
  } catch (error) {
    console.error(error);
  } finally {
    //clean up here
  }
};
fetchData();

Вывод:

Время, затрачиваемое на обработку запроса, составляет 50,36 мс. Это время выполнения может быть увеличено. Чтобы проиллюстрировать преимущества параллелизма, давайте сравним подход с использованием Promise.all()

const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
const apiUrl2 = "https://jsonplaceholder.typicode.com/todos/2";
function fetchData1() {
  console.time("fetch1");
  Promise.all([
    fetch(apiUrl).then((response) => response.json()),
    fetch(apiUrl2).then((response) => response.json()),
  ])
    .then((results) => {
      console.log("Results:", results);
      console.timeEnd("fetch1");
    })
    .catch((error) => {
      console.error("Error fetching data:", error);
    });
}
fetchData1();

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

Вот результат выполнения кода:

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

Если же вам необходимо дождаться выполнения всех Promise, независимо от того, будут ли они выполнены или отклонены, то можно использовать функцию Promise.allSettled().

Отправка нескольких фрагментов данных

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

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

Например, предположим, что нужно проанализировать следующий образец данных:

const data = [
  "I absolutely love this product! It's amazing.",
  "The service was terrible. I won't come back again.",
  "Great experience overall. Would recommend to friends.",
  "Beyond what I expected. The product quality is excellent.",
  "Jazz performance was mind-blowing, I enjoyed every moment.",
];

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

В результате код будет выглядеть примерно так:

const AnalyseReviews = async (textChunk) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const sentiment =
        textChunk.includes("love") || textChunk.includes("great")
          ? "positive"
          : "negative";
      resolve({ text: textChunk, sentiment });
    }, 500);
  });
};
const reviewFeedback = async () => {
  try {
    const results = await Promise.all(
      data.map((chunk) => AnalyseReviews(chunk))
    );
    console.log("results:", results);
  } catch (error) {
    console.error("An error occurred, please try again:", error);
  }
};
reviewFeedback();

Чтение и обработка нескольких файлов одновременно.

Представьте, что у вас есть приложение, которое позволяет пользователям загружать файлы массово. После проверки загруженных файлов, вы можете использовать Promise.all() для параллельного чтения нескольких файлов. Это значительно эффективнее, чем чтение каждого файла по очереди.

Без Promise.all() вам пришлось бы ждать завершения чтения каждого файла перед началом чтения следующего. Это привело бы к увеличению времени обработки, особенно при большом количестве файлов.

С Promise.all() операции чтения файлов запускаются одновременно, что позволяет значительно сократить время обработки и повысить удобство для пользователей.

import { readFile } from "fs/promises";
async function processFiles(filePaths) {
  try {
    const fileContents = await Promise.all(
      filePaths.map((path) => readFile(path, "utf8"))
    );
    return fileContents.map((content) => content.toUpperCase());
  } catch (error) {
    console.error("Error processing files:", error);
    throw error;
  }
}

// Usage
const filePaths = ["file3.txt", "/file3.txt", "file3.txt"];
processFiles(filePaths)
  .then((contents) => console.log(contents))
  .catch((error) => console.error(error));

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

Итоги

Promise.all() предлагает множество преимуществ:

Более понятный код: Упрощает структуру кода, избавляя от вложенных цепочек .then(). Запросы обрабатываются в одном блоке .then().

Эффективность: Обеспечивает более высокую производительность за счет одновременного выполнения запросов, что сокращает общее время, необходимое для получения данных.

Источник:

#JavaScript #Начинающим
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу