Промисы  —  ключевая особенность асинхронного программирования на JavaScript. Независимо от того, как вы к ним относитесь, вам нужно понимать механизм их работы.

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

1. Синхронизированные блоки кода

console.log('start');

const promise1 = new Promise((resolve, reject) => {
console.log(1)
})

console.log('end');

Первая задача очень проста.

Мы знаем следующее.

  • Синхронизированные блоки кода всегда выполняются последовательно сверху вниз.
  • При вызове new Promise(callback) функция обратного вызова выполняется сразу.

Таким образом, этот код должен последовательно вывести start, 1 и end.

console.log(‘start’)

const promise1 = new Promise((resolve, reject) => {
 console.log(1)
})

console.log(‘end’);

2. Появление асинхронного кода

console.log('start');

const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve(2)
})

promise1.then(res => {
console.log(res)
})

console.log('end');

В этом сниппете появляется часть асинхронного кода. Это функция обратного вызова в .then().

Помните, что движок JavaScript всегда сначала выполняет синхронный код, а затем асинхронный.

При столкновении с этой проблемой достаточно различать синхронный и асинхронный код.

Таким образом, выводом будет start, 1, end и 2.

console.log(‘start’);

const promise1 = new Promise((resolve, reject) => {
 console.log(1)
 resolve(2)
})

promise1.then(res => {
 console.log(res)
})

console.log(‘end’);;

3. Метод resolve

console.log('start');

const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve(2)
console.log(3)
})

promise1.then(res => {
console.log(res)
})

console.log('end');

Этот код почти аналогичен предыдущему. Единственное отличие заключается в том, что после resolve(2) идет console.log(3).

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

Таким образом, получаем следующий вывод: start, 1, 3, end и 2.

console.log(‘start’);

const promise1 = new Promise((resolve, reject) => {
 console.log(1)
 resolve(2)
 console.log(3)
})

promise1.then(res => {
 console.log(res)
})

console.log(‘end’);;

Я сделал оговорку насчет метода resolve, так как встречал тех, кто считает, что resolve прервет выполнение функции.

4. Отсутствие вызова метода resolve

console.log('start');

const promise1 = new Promise((resolve, reject) => {
console.log(1)
})

promise1.then(res => {
console.log(2)
})

console.log('end');

В этом коде метод resolve не вызывался, поэтому promise1 всегда находится в состоянии ожидания. promise1.then(…) не выполняется, а 2 не выводится в консоль.

Поэтому результатом вывода будет start, 1 и end.

console.log(‘start’);

const promise1 = new Promise((resolve, reject) => {
 console.log(1)
})

promise1.then(res => {
 console.log(2)
})

console.log(‘end’);;

5. Функция, которая может сбить вас с толку

console.log('start')

const fn = () => (new Promise((resolve, reject) => {
console.log(1);
resolve('success')
}))

console.log('middle')

fn().then(res => {
console.log(res)
})

console.log('end')

Вышеприведенный код словно намеренно добавляет специальную функцию, чтобы запутать программиста-соискателя, и эта функция  —  fn.

Однако стоит помнить, что независимо от количества уровней вызовов функций, основные принципы остаются неизменными.

  • Сначала выполняется синхронный код, затем  —  асинхронный.
  • Синхронный код выполняется в том порядке, в котором он был вызван.

Таким образом, выводом будет start, middle, 1 , end и success.

console.log(‘start’)

const fn = () => (new Promise((resolve, reject) => {
 console.log(1);
 resolve(‘success’)
}))

console.log(‘middle’)

fn().then(res => {
 console.log(res)
})

console.log(‘end’);

6. Выполняющийся промис

console.log('start')

Promise.resolve(1).then((res) => {
console.log(res)
})

Promise.resolve(2).then((res) => {
console.log(res)
})

console.log('end')

В этом случае Promise.resolve(1) вернет объект Promise, состояние которого находится в статусе fulfilled, а результат  —  1. Это синхронный код.

Таким образом, результат на выводе  —  start, end, 1 и 2.

console.log(‘start’)

Promise.resolve(1).then((res) => {
 console.log(res)
})

Promise.resolve(2).then((res) => {
 console.log(res)
})

console.log(‘end’);

Думаете, эти задачи слишком легкие? Подождите, это только начало. Сложность Promise заключается в том, что он появляется вместе с setTimeout. Следующие задачи будут посложнее.

7. Задача на понимание основ обратного вызова

console.log('start')

setTimeout(() => {
console.log('setTimeout')
})

Promise.resolve().then(() => {
console.log('resolve')
})

console.log('end')

Будьте внимательны! Это очень сложный вопрос. Если вы сможете правильно ответить на него и привести корректные объяснения, это значит, что вы понимаете асинхронное программирование на JavaScript на среднем уровне.

Прежде чем разобрать этот вопрос, пройдемся по теории.

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

Кто-то может сказать: выполнится первым то, что первым завершится. Это верно, но что если две асинхронные задачи завершатся одновременно?

Например, в приведенном выше коде таймер setTimeout равен 0 секунд, а Promise.resolve() вернет выполненный объект Promise сразу после выполнения.

Обе асинхронные задачи выполняются немедленно. Какая же функция обратного вызова будет выполнена первой?

Некоторые могут сказать, что setTimeout находится впереди, поэтому сначала выводится setTimeout, а затем  —  resolve. Это утверждение неверно.

Мы знаем, что не всегда выполняется правило “первым пришел  —  первым ушел”. В частности, это касается движения транспорта.

Как правило, мы делим транспортные средства на две категории.

  • Транспорт общего назначения.
  • Транспорт для решения неотложных задач (например, пожарные машины и машины скорой помощи).

Стоя на переполненных дорожных перекрестках, мы будем пропускать пожарные машины и машины скорой помощи в первую очередь. Транспортные средства для чрезвычайных ситуаций имеют более высокий приоритет по сравнению с другими машинами. Ключевое слово здесь  —  приоритет.

В EventLoop также существует понятие приоритета.

  • Задачи с более высоким приоритетом называются микрозадачами. В эту категорию входят Promise, ObjectObserver, MutationObserver, process.nextTick и async/await
  • Задачи с более низким приоритетом называются макрозадачами. К ним относятся setTimeout, setInterval и XHR.

Несмотря на то, что setTimeout и Promise.resolve() завершаются одновременно, и то, что код setTimeout расположен впереди, из-за низкого приоритета функция обратного вызова, относящаяся к нему, выполняется позже.

Таким образом, результат на выводе  —  start, end, resolve и success.

console.log(‘start’)

setTimeout(() => {
 console.log(‘setTimeout’)
})

Promise.resolve().then(() => {
 console.log(‘resolve’)
})

console.log(‘end’);

8. Код для микро- и макрозадач

const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});

promise.then((res) => {
console.log(res);
});

console.log(4);

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

Нужно просто выполнить три действия.

  • Найти код синхронизации.
  • Найти код микрозадачи.
  • Найти код макрозадачи.

Сначала нужно выполнить код синхронизации:

Вывод: 1, 2 и 4.

Затем следует выполнить микрозадачу:

Но здесь кроется ловушка: поскольку текущий промис все еще находится в состоянии ожидания, код в нем не будет выполнен в данный момент.

Затем выполните макрозадачу:

Состояние promise становится fulfilled.

Затем с помощью EventLoop снова выполните микрозадачу:

const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log(“timerStart”);
resolve(“success”);
console.log(“timerEnd”);
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);;

9. Расстановка приоритетов между микро- и макрозадачами

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

const timer1 = setTimeout(() => {
console.log('timer1');

const promise1 = Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)

const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)

Каков вывод этого кода?

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

  1. Сначала выполняются все микрозадачи.
  2. Затем выполняются все макрозадачи.
  3. Снова выполняются все микрозадачи.
  4. Циклический проход.

Но такое видение неверно.

Вот правильный алгоритм.

  1. Сначала выполнение всех микрозадач.
  2. Выполнение макрозадачи.
  3. Снова выполнение всех (вновь добавленных) микрозадач.
  4. Выполнение следующей макрозадачи.
  5. Циклический проход.

Вот пример:

Или вот так:

Поэтому в приведенном выше коде функция обратного вызова Promise.then будет выполнена раньше функции обратного вызова второго setTimeout, поскольку она является микрозадачей и осуществляется вне очереди.

10. Проверка основ понимания промисов

Настало время для последней задачи. Если вы сможете правильно назвать вывод этого кода, это значит, что вы понимаете промисы на продвинутом уровне. И вы с легкостью ответите на подобные вопросы на собеседовании.

Это задание является усовершенствованной версией предыдущего, но основной принцип остается тем же.

Пройдемся еще раз по изученным шагам:

  1. Синхронный код.
  2. Все микрозадачи.
  3. Первая макрозадача.
  4. Все вновь добавленные микрозадачи.
  5. Следующая макрозадача.

Выполните весь синхронный код:

Выполните все микрозадачи:

Выполните первую макрозадачу:

Обратите внимание: на этом этапе макрозадача добавляет новую микрозадачу в очередь задач.

Выполните все вновь добавленные микрозадачи:

Выполните следующую макрозадачу:

Вывод:

console.log(‘start’);

const promise1 = Promise.resolve().then(() => {
 console.log(‘promise1’);
 const timer2 = setTimeout(() => {
 console.log(‘timer2’)
 }, 0)
});

const timer1 = setTimeout(() => {
 console.log(‘timer1’)
 const promise2 = Promise.resolve().then(() => {
 console.log(‘promise2’)
 })
}, 0)

console.log(‘end’);;

Заключение

Чтобы вопросы интервьюера не застали вас врасплох, твердо запомните три правила.

  1. Движок JavaScript всегда сначала выполняет синхронный код, а затем  —  асинхронный.
  2. Микрозадачи имеют более высокий приоритет, чем макрозадачи.

3. Микрозадачи могут выполняться без очереди в EventLoop.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи bytefish: 10 JavaScript Promise Challenges Before You Start an Interview

Предыдущая статьяСоздание расширяющих методов на C#
Следующая статья4 пакета Python для причинно-следственного анализа данных