Возможность писать асинхронный код через async / await, которая появилась в Javascript начиная с версии ES2017, безусловно упрощает процесс разработки и понимания написанного другими кода. Давайте рассмотрим одну неочивидную особенность поведения такой программы.
Начнем с написания кода на старых добрых промисах. Допустим, у нас есть метод getNum, который возвращает разрешившийся Promise с числовым значением, эмулируя таким образом запрос к асинхронному API. Есть еще функция add, которая делает «асинхронный запрос» getNum и результат добавляет к аккумулятору sum. Потом через Promise.all() запускаем сразу несколько «запросов» и в конце выводим в консоль содержимое аккумулятора.
1 2 3 4 5 6 7 8 9 10 11 |
let sum = 0; const getNum = (num) => Promise.resolve(num); const add = (num) => { getNum(num).then((result) => { sum += result; }); } Promise.all( [add(1), add(2)] ).then(() => console.log(sum)); |
Код довольно тривиальный, а учитывая однопоточность Javascript, никаких сюрпризов с параллельным выполнением двух методов мы не получим: в консоль ожидаемо выведется цифра 3.
Давайте теперь перепишем эту программу с использованием async / await:
1 2 3 4 5 6 7 8 9 10 |
let sum = 0; const getNum = async (num) => num; const add = async (num) => { const result = await getNum(num); sum += result; } Promise.all( [add(1), add(2)] ).then(() => console.log(sum)); |
Действительно, стало выглядеть попроще! А результат в консоль выводится тот же — цифра 3. Теперь немного упростим функцию add, переменная (ну то есть константа) result нам, вроде как, ни к чему, избавимся от нее:
1 2 3 4 5 6 7 8 9 |
let sum = 0; const getNum = async (num) => num; const add = async (num) => { sum += await getNum(num); } Promise.all( [add(1), add(2)] ).then(() => console.log(sum)); |
Снова запускаем код в консоли, и, о ужас, получаем в результате цифру 2! Как такое возможно? Мы что, сломали однопоточную модель JS, и теперь оба вызова add() выполняются параллельно, второй затирает результат работы первого?
Чтобы лучше разобраться в причинах такого поведения, попробуем транспилировать наш современный код в код стандарта ES5 для старых браузеров через Babel (TypeScript транспилирует подобным же образом), вырежу самую интересную часть:
1 2 3 4 5 6 7 8 9 |
... case 0: _context2.t0 = sum; _context2.next = 3; return getNum(num); case 3: sum = _context2.t0 += _context2.sent; ... |
Тут мы видим, что перед вызовом асинхронной функции getNum содержимое аккумулятора sum запоминается во временной переменной, а затем, уже после получения результата промиса, именно это сохраненное значение используется при суммировании.
Сохранение значения аккумулятора происходит в начале вызова метода add, в этот момент при обоих вызовах значение sum равно 0. Cуммирование и сохранение обратно в аккумулятор происходит уже после выполнения всех вызовов add, поскольку getNum — метод асинхронный. Получается, что каждое такое суммирование не учитывает предыдущие операции и сохраняется только результат последней операции.
Какой вывод можно сделать? Оператор await не просто останавливает выполнение программы в данной точке, он еще и «замораживает» составные части текущего выражения. Из этого следует практический совет: используйте await только для присваивания результата асинхронного вызова переменной (как в моем первом примере с async / await), а затем используйте эту переменную в дальнейших операциях.
Много всяких более практичных подводных камней JS можно еще найти в статье: Шпаргалка по операторам сравнения и преобразованию типов в JS.