Некоторые особенности async / await

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

Начнем с написания кода на старых добрых промисах. Допустим, у нас есть метод getNum, который возвращает разрешившийся Promise с числовым значением, эмулируя таким образом запрос к асинхронному API. Есть еще функция add, которая делает «асинхронный запрос» getNum и результат добавляет к аккумулятору sum. Потом через Promise.all() запускаем сразу несколько «запросов» и в конце выводим в консоль содержимое аккумулятора.

Код довольно тривиальный, а учитывая однопоточность Javascript, никаких сюрпризов с параллельным выполнением двух методов мы не получим: в консоль ожидаемо выведется цифра 3.

Давайте теперь перепишем эту программу с использованием async / await:

Действительно, стало выглядеть попроще! А результат в консоль выводится тот же — цифра 3. Теперь немного упростим функцию add, переменная (ну то есть константа) result нам, вроде как, ни к чему, избавимся от нее:

Снова запускаем код в консоли, и, о ужас, получаем в результате цифру 2! Как такое возможно? Мы что, сломали однопоточную модель JS, и теперь оба вызова add() выполняются параллельно, второй затирает результат работы первого?

Чтобы лучше разобраться в причинах такого поведения, попробуем транспилировать наш современный код в код стандарта ES5 для старых браузеров через Babel (TypeScript транспилирует подобным же образом), вырежу самую интересную часть:

Тут мы видим, что перед вызовом асинхронной функции getNum содержимое аккумулятора sum запоминается во временной переменной, а затем, уже после получения результата промиса, именно это сохраненное значение используется при суммировании.

Сохранение значения аккумулятора происходит в начале вызова метода add, в этот момент при обоих вызовах значение sum равно 0. Cуммирование и сохранение обратно в аккумулятор происходит уже после выполнения всех вызовов add, поскольку getNum — метод асинхронный. Получается, что каждое такое суммирование не учитывает предыдущие операции и сохраняется только результат последней операции.

Какой вывод можно сделать? Оператор await не просто останавливает выполнение программы в данной точке, он еще и «замораживает» составные части текущего выражения. Из этого следует практический совет: используйте await только для присваивания результата асинхронного вызова переменной (как в моем первом примере с async / await), а затем используйте эту переменную в дальнейших операциях.

Много всяких более практичных подводных камней JS можно еще найти в статье: Шпаргалка по операторам сравнения и преобразованию типов в JS.

Добавить комментарий