Иногда мы часто склонны передавать асинхронные функции с функциями forEach и map.

Например:

[1,2,3,4,5].forEach(async (n) => setTimeout(console.log(n), 1000));

Ожидается: Ожидается, что все числа от 1 до 5 будут напечатаны один за другим с промежутком в 1 секунду после печати каждой строки.

Результат:Все числа от 1 до 5 печатаются одно за другим, но отсутствует промежуток в 1 секунду после печати каждой строки.

Почему это происходит?

Посмотрите на эту функцию javascipt:

async function doubleOf(n) {
  return n*2;
}

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

Результат: фактически возвращается обещание, которое преобразуется в число.

Если мы напишем эквивалент этой функции на машинописном языке со строгими типами, все станет ясно.

Следующий код не будет компилироваться:

async function doubleOf(n: number): number {
  return n*2;
}

поскольку это асинхронная функция, а асинхронные функции возвращают только объекты типа Promise, которые могут разрешаться в значение (включая null). Асинхронная функция не возвращает такие значения, как числа, строки и т. д.

Правильная версия будет:

async function doubleOf(n: number): Promise<number> {
  return n*2;
}

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

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

function doubleOf(n) {
  return new Promise((resolve) => resolve(n*2));
}

Или эквивалент машинописного текста:

function doubleOf(n: number): Promise<number> {
  return new Promise((resolve) => resolve(n*2));
}

Более четкое изображение:

  1. У нас есть функция doubleOf, которая принимает число и возвращает число. Обычный старый javascript.
  2. У нас есть функция doubleOfOldWay, которая принимает число и возвращает обещание, которое преобразуется в число.
  3. У нас есть doubleOfNewWay, асинхронная функция, которая принимает число и кажется, что возвращает число, но на самом деле она возвращает обещание, которое преобразуется в число, как и функция doubleOfOldWay.
  4. Функции doubleOfOldWay и doubleOfNewWay абсолютно одинаковы.
  5. И, следовательно, когда мы пытаемся выполнить операцию умножения значений, возвращаемых функциями doubleOfOldWay и doubleOfNewWay, результатом будет NaN, поскольку мы не можем умножать промисы (очевидно!).
  6. Чтобы умножить doubleOfOldWay и doubleOfNewWay:

(await doubleOfOldWay(5)) * (await doubleOfNewWay(6)) это приведет к 120

Итак, вернемся к нашему первоначальному примеру:

[1,2,3,4,5].forEach(async (n) => setTimeout(console.log(n), 1000));

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

Самый правильный способ реализовать то, что мы ожидаем от этой функции forEach, — использовать простой цикл for:

for(const number of [1,2,3,4,5]) {
    console.log(number);
    await new Promise(resolve => setTimeout(resolve, 1000)); // Sleep for "atleast" 1 second
}

Еще один простой пример:

[1,2,3,4,5].map(async (n) => n*2);

К настоящему времени должно быть ясно, что вывод этой строки кода будет:

(5) [Promise, Promise, Promise, Promise, Promise]
0: Promise {<fulfilled>: 2}
1: Promise {<fulfilled>: 4}
2: Promise {<fulfilled>: 6}
3: Promise {<fulfilled>: 8}
4: Promise {<fulfilled>: 10}

массив промисов, которые разрешают числа, а не массив чисел.

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

await Promise.all([1,2,3,4,5].map(async (n) => n*2));

это приведет к:

[2, 4, 6, 8, 10]