Написать пост

Решаем популярные задачи с асинхронным кодом на JavaScript: часть первая

Логотип компании Elbrus Bootcamp

В статье разобрали популярные задачи с асинхронным кодом, которые могут попасться начинающему Frontend-разработчику на собеседованиях.

На собеседованиях начинающим Frontend-разработчикам часто попадаются задачи на асинхронный код. Преподаватель Elbrus Bootcamp Денис Образцов выбрал несколько популярных задач, с которыми наши выпускники часто сталкиваются на интервью, и разобрал логику их решения. 

В первой части текста вспомним, как устроен цикл событий и разберём несколько базовых задач на логику и внимательность. Во второй части перейдём к более сложным примерам на порядок попадания задач в Event Loop и оптимизацию кода.

Как устроен цикл событий

Машина читает код дважды: сначала в память компьютера записываются переменные, потом происходит непосредственное выполнение кода. Часто задач в коде несколько: они попадают в цикл событий (Event Loop) и выполняются в определённой последовательности.

Последовательность задаётся типом кода: синхронным или асинхронным. В случае с синхронным кодом все задачи попадают сразу в Call Stack и выполняются по очереди.

Асинхронный код используется, например, когда программе нужно обратиться к базе данных или другому внешнему источнику информации. Этот процесс можно сравнить с телефонным звонком: когда вы звоните кому-то, вы заранее не знаете, когда вам ответят — после первого гудка, после пятого или вообще не возьмут трубку.

С асинхронным кодом сложнее: во-первых, он всегда выполняется после синхронного, а во-вторых, делится ещё на две очереди — макро- и микрозадачи.

Микрозадачи — в основном, промисы, которые выполняются в первую очередь. Большие задачи (например, таймеры, AJAX-запросы) попадают в самый конец стека и выполняются последними.

Теперь, когда мы вспомнили теорию, перейдём к разбору задач на понимание асинхронного кода, которые могут попасться на собеседовании. Все задачи рассчитаны на джунов и собраны командой Elbrus Bootcamp на реальных интервью.

Решаем популярные задачи с асинхронным кодом на JavaScript: часть первая 1

Больше задач и историй студентов — в нашем Telegram-канале @Elbrus Bootcamp

Задача первая

			// В каком порядке будут выведены консоли и какие именно?
const p = new Promise((resolve, reject) => {
  reject(Error('Всё сломалось :('));
})
  .catch((error) => console.log('1-я', error.message))
  .catch((error) => console.log('2-я', error.message));
		

Прежде чем разбирать код, рассмотрим пример, к которому мы будем возвращаться на протяжении всей статьи. Он поможет глубже понять принцип работы асинхронного кода.

Представьте, что вы пришли в фастфуд, сделали заказ, получили специальный пульт и ждёте, пока пульт завибрирует и можно будет пойти и забрать заказ.

В первой строке кода мы видим promise — это специальный объект, который даёт обещание, что в будущем будет выполнено то или иное действие. Promise выступает аналогом такого пульта и в данном случае обещает уведомить не о готовности заказа, а об ошибке, если она возникнет.

Под promise прописан вариант развития событий — reject, который выводит в консоль сообщение «Всё сломалось» в случае, если что-то пошло не так. Второй, положительный, вариант resolve в этой задаче не указан.

Вернёмся к примеру с фастфудом: вы сели за столик и ждёте заказ. Через некоторое время пульт завибрировал. Дальше может быть несколько вариантов развития событий: вы успешно получите заказ или кассир позовёт вас сообщить, что какого-то ингредиента нет и блюдо не смогут приготовить. На этот случай и нужны resolve и reject.

Следующий шаг — встать и подойти к стойке. За него отвечают обработчики .catch, которые в коде идут в цепочке друг за другом. Это важный момент: между обработчиками нет точки с запятой, и цепочка идёт сразу после объявления переменной ‘p’, поэтому выполняется только первый .catch. Второй выполняет те же действия и не срабатывает.

Это сравнительно простая задача: в ней нет смешивания синхронного и асинхронного кода. В консоли мы получим результат выполнения promise, а затем — вывод первого .catch.

Дополнение первое

			const p2 = new Promise((resolve, reject) => {
  reject(Error('Всё сломалось :('));
});
// тут обе консоли, потому что нет цепочки, каждый catch отрабатывает отдельно
p2.catch((error) => console.log('3-я', error.message));
p2.catch((error) => console.log('4-я', error.message));
		

Здесь происходит то же самое, что и в базовом варианте задачи, но с исключениями. Есть два обращения к константе ‘p2’, нет цепочки, между .catch появилась точка с запятой, поэтому в консоль выводится результат обоих обработчиков.

Стоит отметить, что смысла в этом немного: обработчики отлавливают одну и ту же ошибку. Но эта задача скорее на внимательность, чем на логику.

Дополнение второе

			const p3 = new Promise((resolve, reject) => {
  reject(Error('Всё сломалось :('));
})
  .then((error) => console.log('5-я', error.message)) // ? бесполезный обработчик положительного ответа
  .catch((error) => console.log('6-я', error.message)); // ? будет отлов ошибки
		

В этой версии задачи есть then. Здесь это обработчик положительного результата (resolve), который не выполняет никакую функцию, в этом коде он бесполезен. В тексте задачи по-прежнему упоминается только негативное развитие событий. Поэтому вывод в консоль будет тот же, что и в предыдущей задаче.

Задача два

			// в каком порядке будут выведены консоли и что в них будет?
setTimeout(() => {
  console.log('timeout')
}, 0);

const p = new Promise((resolve, reject) => {
  console.log('Promise creation');
  resolve()
})

const p2 = new Promise((resolve, reject) => {
  console.log(123)
})

p.then(() => {
  console.log('Promise resolving');
})

console.log('End')

console.log('p2 =>>', p2)
		

Разберём текст задачи. В первой строчке указан таймер setTimeout с нулевой задержкой, следом идут два promise: c пустой функцией обработки положительного ответа и без функции.

Здесь then — обработчик первого promise, который получает результат выполнения resolve. В последних строчках — консоль завершения и консоль, которая выводит результат выполнения второго promise.

Вспомним, в каком порядке код попадает в Call Stack. В первую очередь выполняется синхронный код: console.log или promise. По дефолту они не асинхронные, пока вы не сделаете их таковыми (например, добавите .catch или .then).

Таким образом, вывод консоли будет иметь следующий порядок:

			setTimeout(() => {
  console.log('timeout') // 6) макрозадача, timeout
}, 0);
const p = new Promise((resolve, reject) => {
  console.log('Promise creation'); // 1) синхронно, Promise creation
  resolve()
})

const p2 = new Promise((resolve, reject) => {
  console.log(123) // 2) синхронно, 123
})

p.then(() => {
  console.log('Promise resolving'); // 5) пришёл из микрозадачи после всего синхронного кода, Promise resolving
})

console.log('End') // 3) синхронно, End

console.log('p2 =>>', p2) // 4) синхронно, Promise {  }
		

Строки с консолями внутри promise выполнятся в первую очередь, поскольку в них нет ничего асинхронного, так как promise сам по себе изначально синхронный. Затем выполняются синхронная консоль ‘End’ и консоль, в которой показывается второй promise, находящийся в стадии ожидания ().

Далее выполняется .then. В базовом варианте promise выполняется синхронно. Только после того, как весь синхронный код отработал, выполняется его асинхронный обработчик. В последнюю очередь выполнится макрозадача с setTimeout.

Задача три

			// todo в каком порядке будут выведены консоли и что в них будет?
console.log('script start'); // ? 1) синхронно, script start


setTimeout(function() {
  console.log('setTimeout'); // ? 5) макрозадача, setTimeout
}, 0);


Promise
  .resolve()
  .then(function() {
  console.log('promise1'); // ? 3) микрозадача, promise1
})
  .then(function() {
  console.log('promise2'); // ? 4) микрозадача, promise2
});


console.log('script end'); // ? 2) синхронно, script end
		

В этой задаче первый и последний console.log синхронные, поэтому они выполнятся сразу. Следом идёт promise с двумя обработчиками, которые выстроены в цепочку и выполняются друг за другом. Обратите внимание, что между ними нет точки с запятой. В последнюю очередь выполняется setTimeout, поскольку это макрозадача.

Эти задачи рассчитаны на базовое понимание работы асинхронного кода. В следующей части статьи разберём более сложные кейсы и оптимизируем скорость выполнения задач в Call Stack.

Реклама ООО “Эльбрус Буткемп”

Следите за новыми постами
Следите за новыми постами по любимым темам
13К открытий13К показов