Когда не использовать async/await или избегать связанных асинхронных задач

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

Введение

Рассмотрим следующий пример:

const user = await getUserById(id);
const products = await getProducts();
return {user, products};

Здесь мы извлекаем конкретные данные о пользователе, а затем извлекаем список продуктов. Вы можете сказать: что в этом плохого? дело в том, что мы ждем, чтобы получить информацию о пользователе, прежде чем получить список продуктов, хотя получение списка продуктов не зависит от списка пользователей. . Так зачем ждать, если мы можем запускать их параллельно? Кроме того, это снижает производительность.

Теперь рассмотрим этот пример:

(async () => {
    const user = await getUserById(id);
    const products = await getProducts();
    return {user, products};
})()

Мы обернули его с помощью IIFE, но все же этот код будет запускать функции getUserById() и getProducts() одну за другой, а не одновременно. Это связано с тем, что выражения await в функции приостанавливают выполнение до тех пор, пока обещания, возвращаемые функциями getUserById() и getProducts(), не разрешатся.

Выражение await можно использовать только внутри функции async, и оно заставляет функцию приостанавливаться до тех пор, пока обещание, переданное ей в качестве аргумента, не будет разрешено. Это позволяет писать асинхронный код, который выглядит и ведет себя как синхронный код, что упрощает его чтение и понимание. Однако, если вы хотите выполнять промисы одновременно, вы можете использовать Promise.all или Promise.allSettled, что позволяет дождаться разрешения или отклонения нескольких промисов в в то же время в чистом виде.

Например, вы можете переписать код следующим образом для одновременного выполнения функций getUserById() и getProducts():

(async () => {
  const [user, products] = await Promise.all([getUserById(id), getProducts()]);
  return {user, products};
})()

Это приведет к одновременному выполнению функций getUserById() и getProducts(), и функция приостановится на выражении await, пока оба промиса не будут разрешены. Разрешенные значения будут деструктурированы в переменные user и products, и функция вернет объект, содержащий эти переменные, в качестве свойств.

Просто, элегантно 😄 и вдвое быстрее, потому что Promise.all выполняет их все одновременно.

Вы можете узнать больше о Promise.all или Promise.allSettled на MDN

Примечание. Стоит отметить, что Promise.all вернется либо в случае успешного выполнения всех обещаний, либо в случае отклонения первого из них, в то время как Promise.allSettled будет ждать, пока каждое обещание не будет разрешено или отклонено, поэтому вы можете взглянуть на Promise.allSettled, который не бросает на отказы.

Для целей этой истории я буду использовать несколько примеров методов:

  • res(ms) — это функция, которая принимает целое число миллисекунд и возвращает обещание, которое разрешается после этого количества миллисекунд.
  • rej(ms) — это функция, которая принимает целое число миллисекунд и возвращает промис, который отклоняется после этого количества миллисекунд.

Звонок res запускает таймер. Использование Promise.all для ожидания нескольких задержек разрешится после завершения всех задержек, но помните, что они выполняются одновременно:

Пример №1

const data = await Promise.all([res(3000), res(2000), res(1000)]);
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========O                     delay 3
//
// =============================O Promise.all

Это означает, что Promise.all разрешится с данными из внутренних промисов через 3 секунды.

Но, Promise.all ведет себя как «сбой быстро»:

Пример #2

const data = await Promise.all([res(3000), res(2000), rej(1000)]);
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =========X                     Promise.all

Если вместо этого вы используете async-await, вам придется ждать последовательного разрешения каждого промиса, что может быть не так эффективно:

Пример №3

const delay1 = res(3000);
const delay2 = res(2000);
const delay3 = rej(1000);
const data1 = await delay1;
const data2 = await delay2;
const data3 = await delay3;
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =============================X await

Важно учитывать использование async/await и избегать ненужного ожидания, чтобы повысить производительность вашего кода. В приведенном примере показано, что использование метода Promise.all может обеспечить параллельное выполнение промисов и сократить общее время, необходимое для разрешения промисов.

Один из сценариев, в котором лучше избегать использования async/await, — это когда результат одного промиса не зависит от результата другого промиса. В этом случае было бы более эффективно выполнять промисы одновременно, используя Promise.all или аналогичный метод, а не ждать последовательного разрешения каждого промиса с помощью async/await.

Кроме того, важно учитывать «быстрое сбой» поведения Promise.all, которое будет отклонено, как только одно из промисов в массиве будет отклонено. Напротив, использование async/await заставит код ждать разрешения или отклонения всех промисов, прежде чем двигаться дальше. В некоторых случаях может быть более подходящим использовать Promise.allSettled, который будет ждать разрешения или отклонения всех промисов перед тем, как двигаться дальше, и не выдавать отказы.

Также важно учитывать потенциальное влияние на производительность при использовании async/await в цикле. В этом случае может быть более эффективно использовать цикл for...of, а не цикл for...in, так как последний потребует использования await и будет выполнять промисы последовательно.

В заключение, хотя async/await может обеспечить чистый и удобный способ управления асинхронным поведением, важно учитывать его использование и потенциальное влияние на производительность. В некоторых случаях использование Promise.all или подобного метода может оказаться более эффективным и повысить производительность вашего кода.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.