И 3 разных способа заставить его работать
Посмотрим правде в глаза: с момента появления шаблона async / await мы пытались использовать его повсюду.
Давно прошли и (почти) забыты времена больших цепочек обратных вызовов javascript и Promises.resolve: теперь все функции начинаются с async
, даже если он не будет содержать асинхронного кода ... на всякий случай :)
Но некоторые части Javascript еще не готовы к работе с этим шаблоном. Одна из этих частей - .forEach
(другие методы, которые не идут вместе с async / await, - это .map
, .filter
и .reduce
, но я напишу об этом другую статью).
.forEach - это метод массива, который позволяет вызывать функцию для каждого элемента массива. Легко, правда?
Итак, это пример .forEach
:
Если я сохраню этот скрипт в файле food.js, а затем запустил node food.js
, на выходе будет следующий результат:
Итак, все правильно: функция вызывалась для каждого элемента массива, и порядок массива совпадает с порядком вывода console.log. Ура! ✨
Но что произойдет, если функция сложнее, чем отдельный console.log?
Код, который мы написали в предыдущем разделе, великолепен, но я хочу большего. Я хочу знать класс моих любимых блюд (мясо, рыба, овощи, фрукты).
Допустим, что я создал в javascript некоторый сложный алгоритм, который, учитывая во входных данных имени продукта, может возвращать класс. Довольно здорово, а? Это мой лучший код на свете. 🚀
Этот алгоритм использует много вычислительной мощности и ресурсов БД, поэтому он вернет обещание.
Очевидно, что я не могу поделиться с вами этим невероятным кодом здесь (и нет, я не буду отвечать на любое проявление неосмотрительности, что этого алгоритма даже не существует), поэтому для этой статьи я буду использовать фальшивый функция, которая имитирует это.
Чтобы вызвать эту функцию, я буду использовать await
внутри моего .forEach
. Немного изменим предыдущий код:
Поскольку использование async await в глобальном состоянии еще не является стандартом, я создал функцию run, которая обертывает код. Внутри функции .forEach (теперь это асинхронная функция) мы вызываем AIFoodRecognition с await
и печатаем результат.
Легко, правда? Результат должен начинаться с «Старт», затем будут напечатаны все мои любимые классы еды, а затем напечатан «Конец».
Давай попробуем:
Ну… не совсем то, что мы ожидали. Это связано с тем, что .forEach вызывает только функцию, она не ждет ее завершения, и ожидается, что каждая функция AIFoodRecognition завершится не менее чем через 500 мс, что намного больше, чем время, которое основной поток потратит на завершение цикла .forEach и печать End . Это облом!
К счастью, у этой проблемы есть несколько решений.
- перепишите .forEach для поддержки await / async;
- использовать for () цикл;
- используйте Promise.all;
Начнем с наименее удобного, даже если он кажется лучшим:
Перепишите .forEach для поддержки await / async
Кажется, это отличное решение! Давайте найдем код для полифила .forEach, давайте обернем все асинхронной функцией, а затем ... Прибыль! 💰💰💰
Это простой .forEach
полифилл:
Давайте изменим его для поддержки async / await:
Как видите, мы добавили async перед функцией в строке 1, а затем перед callback.call мы будем использовать await. Мы собираемся заменить метод forEach в Array.prototype этим методом.
Ok! Теперь попробуем использовать это в нашем предыдущем коде:
И после повторного запуска вывод будет…
ОЙ.
Это потому, что мы забыли кое-что важное: теперь forEach - это асинхронная функция, поэтому, чтобы дождаться его завершения, нам нужно использовать await перед его вызовом.
Если мы просто добавим await
перед forEach, как это
И у нас есть правильный результат:
Поэтому нам нужно только добавить await везде, где мы уже использовали .forEach, и обернуть его асинхронной функцией… не стоит того.
Кроме того, плохая практика - переопределять некоторые методы класса по умолчанию, поскольку в будущем вы можете забыть об этом переопределении, или другие разработчики могут прыгнуть внутрь проекта и потратить часы или дни, пытаясь найти ошибку и найти способ отомстить на вас за это.
Давайте попробуем найти другие решения, может быть, менее хакерские.
Используйте наш старый добрый цикл for ()
Месть старого за () цикл. В прошлом мы везде заменяли его новым, более красивым .forEach, а теперь возвращаемся, прося прощения.
Попробуем использовать это в нашем коде:
И… это работает с нашей первой попытки!
Немного лучше: использовать для… из
Одним из вариантов цикла for () является использование шаблона for… of:
Но я думаю, что есть что-то еще лучшее, что, возможно, поможет нам сократить время ожидания.
Используйте Promise.all, чтобы сократить время работы 🕐
До сих пор, чтобы завершить нашу программу, мы ждали 3 секунды (500 мс для каждого элемента в foodArray). Это потому, что ожидание было внутри цикла. Но что, если бы у нас было 100 элементов в foodArray? Или 1000? Время ожидания было бы ужасным.
Может быть, есть способ сделать все распознавание еды параллельно, вместо того, чтобы ждать завершения каждого, прежде чем начинать следующий? Да, есть: он называется Promise.all. Этот метод Promise принимает массив в качестве параметра и будет возвращать только тогда, когда все элементы внутри массива завершены, возвращая новый массив результатов.
Мы собираемся немного изменить код, но я обещаю, что оно того стоит:
Итак, теперь у нас есть новый массив с именем promises, и внутри .forEach мы собираемся добавить каждый вызов функции AIFoodRecognition, таким образом мы запускаем каждое распознавание параллельно. Помните: AIFoodRecognition возвращает Promise, поэтому мы по-прежнему не можем получить наши классы из этого массива. Для этого нам нужно использовать Promise.all с единственным ожиданием, которое вернет новый массив, содержащий возвращаемые значения каждого элемента нашего массива обещаний. Теперь нам нужно только циклически перебрать этот массив, чтобы распечатать результаты, и все готово! ✨
Давайте посмотрим на разницу между кодом с Promise.all и с циклом for () (я установил задержку 1500 мс вместо 500 мс для этих гифок)
Как видите, метод Promise.all намного быстрее, чем цикл for (). Поэтому, если вам не нужно что-то из выходного результата предыдущего цикла, вы всегда должны использовать версию Promise.all для запуска некоторого кода async / await в цикле.
Дайте мне знать, что вы думаете, или если у вас есть другие способы использовать ожидание внутри цикла, вы всегда можете найти меня в Twitter 💛