Решаем популярные задачи с асинхронным кодом на JavaScript: часть вторая
В первой части текста мы вспомнили, как устроен цикл событий, и разобрали несколько простых задач на асинхронное программирование на JavaScript. В этой статье преподаватель Эльбрус Буткемп Денис Образцов рассмотрит более сложные примеры на порядок попадания задач в Event Loop и оптимизацию кода.
Задача первая
let a;
let p4 = new Promise(function (resolve) {
console.log('TEST A1', a);
a = 25;
setTimeout(() => {
console.log('TEST A2', a);
resolve(a);
}, 100);
});
setTimeout(function timeout() {
a = 10;
console.log('TEST A3', a);
}, 100);
p4.then(function (b) {
console.log('TEST A4', a);
});
console.log('TEST A5', a);
Разберём задачу подробно. В первой строке объявляется переменная без присвоения значения, после которой создается promise и вывод в консоль значения a. Все перечисленные строки — это синхронный код, поскольку сам по себе promise не является асинхронным.
После этого ей присваивается значение: a = 25. Следующим шагом появляется таймер setTimeout, внутри которого вывод значения переменной в консоль и функция resolve, в которую вкладывается переменная ‘a’ для успешного исполнения promise c задержкой в 100 миллисекунд.
Если предыдущий таймер находился внутри promise, то второй setTimeout написан как отдельная функция. Здесь переменной ‘a’ присваивается новое значение, которое выводится в консоль с задержкой в 100 миллисекунд. На последних трёх строчках — обработка promise через then, которая завершается выводом консоли. Очередность вывода и переменная ‘a’ будут выглядеть следующим образом:
let a;
let p4 = new Promise(function (resolve) {
console.log('TEST A1', a); // 1)синхронно, a = undefined
a = 25;
setTimeout(() => {
console.log('TEST A2', a); // 3)асинхронно, a = 25
resolve(a);
}, 100);
});
setTimeout(function timeout() {
a = 10;
console.log('TEST A3', a); // 5)асинхронно, a = 10
}, 100);
p4.then(function (b) {
console.log('TEST A4', a); // 4)асинхронно, a = 25
});
console.log('TEST A5', a); // 2)синхронно, a = 25
С точки зрения очередности самая запутанная часть задачи — promise, внутри которого находится setTimeout. Базово таймер относится к макрозадачам, которые попадают в Call Stack последними и выполняются в последнюю очередь. Поскольку в этой задаче он находится внутри promise — микрозадачи, которая выполняется раньше, — в вывод он попадёт третьим. Сразу после того, как отработал весь синхронный код.
Если говорить о значениях, которые выводятся в консоль, то в первой строке будет undefined, поскольку никакое значение на этом этапе переменной не присвоено. Это произойдет только в следующей строке. Консоль, которая выполняется во вторую очередь, выдаст значение 25, так как она расположена в конце кода, и к моменту ее появления значение переменной уже присвоено. В следующих выводах в консоль выводится значение переменной.
Задача вторая
// todo Объяснить код, рассказать какие консоли и в какой последовательности будут, а затем предложить более оптимальное решение
function resolveAfter2Seconds(x) {
console.log(`Какой Х пришёл -> ${x}`)
return new Promise(resolve => {
setTimeout(() => {
resolve(x); //
}, 5000);
});
}
async function add1(x) {
console.log('add1 Hello')
const a = await resolveAfter2Seconds(20);
const b = await resolveAfter2Seconds(30);
console.log('add1 Bye')
return x + a + b;
}
add1(10).then(console.log);
В этой задаче появляется функция async. Прежде чем приступить к разбору, вспомним, для чего она нужна.
Функция async позволяет работать с асинхронным кодом так, будто он синхронный. Синхронный код не работает с асинхронным, но в пределах функции async возможно сделать вид, что так можно. Для этой задачи важно отметить, что любая async-функция сразу же возвращает promise. В паре с async идёт ключевое слово await, которое буквально означает «дождись». Сама по себе async-функция, как и promise — не является асинхронной.
Перейдём к разбору кода. В первой строке находится функция resolveAfter2Seconds, которая принимает аргумент x, выводит его значение в консоль и возвращает его в promise с задержкой в пять секунд.
Далее следует функция async. Внутри нее — синхронная консоль, за которой следуют две асинхронные функции, которые «дожидаются» своего выполнения через await. Результат их поочередного выполнения попадает в promise, а аргумент x приобретает значение сначала ‘a’, а затем — ‘b’. Затем выводится результат Bye.
В последней строчке функции указан вывод результата. Так как любая async возвращает promise, в return не удастся получить конкретного результата. Вместо него мы получим promise pending — ожидание. Then в последней строчке — обработчик этого pending.
В результате порядок выполнения будет следующим:
function resolveAfter2Seconds(x) {
console.log(`Какой Х пришёл -> ${x}`) // 2) Какой Х пришёл -> 20 undefined 3) Какой Х пришёл -> 30
return new Promise(resolve => {
setTimeout(() => {
resolve(x); //
}, 5000);
});
}
async function add1(x) {
console.log('add1 Hello') // 1)add1 Hello
const a = await resolveAfter2Seconds(20);
const b = await resolveAfter2Seconds(30);
console.log('add1 Bye') // 4)add1 Bye
return x + a + b;
}
add1(10).then(console.log); // 5)60
Этот код работает медленно: чем больше внутри async-функции add1 вызовов функций resolveAfter2Seconds с ожиданием (await), тем больше времени займёт его выполнение. Если вызовов два, то он будет выполняться 10 секунд. Если их будет 10, ждать придётся почти минуту.
Попробуем переписать код и сократить время ожидания. Поставим все вызовы в очередь без await, чтобы они выполнялись все одновременно за пять секунд:
async function add2(x) {
console.log('add2 Hello')
const p_a = resolveAfter2Seconds(200);
const p_b = resolveAfter2Seconds(300);
const p_c = resolveAfter2Seconds(100);
const p_d = resolveAfter2Seconds(999);
console.log('add2 Bye')
return x + await p_a + await p_b + await p_c + await p_d;
}
add2(400).then(console.log);
Теперь мы дожидаемся выполнения всех функций уже в конце и только после этого показываем конечный результат. В результате оптимизации ждать придётся не 20 секунд, а всего пять.