Иногда мы часто склонны передавать асинхронные функции с функциями 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)); }
Более четкое изображение:
- У нас есть функция
doubleOf
, которая принимает число и возвращает число. Обычный старый javascript. - У нас есть функция
doubleOfOldWay
, которая принимает число и возвращает обещание, которое преобразуется в число. - У нас есть
doubleOfNewWay
, асинхронная функция, которая принимает число и кажется, что возвращает число, но на самом деле она возвращает обещание, которое преобразуется в число, как и функцияdoubleOfOldWay
. - Функции
doubleOfOldWay
иdoubleOfNewWay
абсолютно одинаковы. - И, следовательно, когда мы пытаемся выполнить операцию умножения значений, возвращаемых функциями
doubleOfOldWay
иdoubleOfNewWay
, результатом будетNaN
, поскольку мы не можем умножать промисы (очевидно!). - Чтобы умножить
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]