Promise — это отличительная особенность JavaScript как асинхронного языка программирования. Нравится вам это или нет, понять его в любом случае придется.
В этой статье я привожу 10 примеров кода с Promise, начиная от базового уровня заканчивая продвинутым. Готовы? Начнем!
Задача №1: Конструктор Promise
Каким будет вывод этого фрагмента кода?
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: .then()
Каким будет вывод этого фрагмента кода?
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 всегда сначала выполняет синхронный код, а затем асинхронный.
При столкновении с этой проблемой нам нужно только различать синхронный и асинхронный код.
Результат
console.log('start');
const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve(2)
})
promise1.then(res => {
console.log(res)
})
console.log('end');;
Итак, выводом будет start
, 1
, end
и 2
.
Задача №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
прервет выполнение функции, поэтому я подчеркиваю этот момент здесь.
Задача №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
всегда находится в состоянии ожидания (pending). Так что 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: с Fulfilling Promise
Каким будет вывод этого фрагмента кода?
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: setTimeout vs Promise
Каким будет вывод этого фрагмента кода?
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
. На самом деле, это утверждение неверно.
Мы знаем, что многие вещи НЕ выполняются в порядке по принципу «первым пришел — первым вышел», например, трафик.
Приоритет
Обычно мы делим весь транспорт на две категории:
Общие транспортные средства.
Транспортные средства для чрезвычайных ситуаций. Например, пожарные машины и машины скорой помощи.
Чтобы проехать многолюдные перекрестки, мы пропустим первыми пожарные машины и машины скорой помощи. Автомобили скорой помощи имеют приоритет выше, чем другой транспорт. Ключевое слово: приоритеты.
В JavaScript EventLoop также есть понятие приоритета.
Задачи с более высоким приоритетом называются микрозадачами. Например:
Promise
,ObjectObserver
,MutationObserver
,process.nextTick
,async/await
.Задачи с более низким приоритетом называются макрозадачами. Например:
setTimeout
,setInterval
иXHR
.
Хотя setTimeout
и Promise.resolve()
выполняются одновременно, и даже код setTimeout
еще впереди, но из-за низкого приоритета относящаяся к нему коллбэк-функция выполняется позже.
Результат
Выходным результатом будет start
, end
, resolve
и setTimeout
.
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 все еще находится в состоянии ожидания (pending), код в данный момент выполняться не будет.
Затем выполните макрозадачу:
И состояние promise
становится fulfilled
.
Затем с помощью Event Loop снова выполните микрозадачу:
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)
Анализ
Некоторые могут подумать, что микрозадачи и макрозадачи выполняются так:
Сначала выполняются все микрозадачи.
Выполняются все макрозадачи.
Выполняются все микрозадачи снова.
Цикл повторяется / Цикл завершается.
Но это утверждение неверно. Правильно вот так:
Сначала выполняются все микрозадачи.
Выполняется одна макрозадача.
Повторно выполняются все (вновь добавленные) микрозадачи.
Выполняется следующая макрозадача.
Цикл повторяется / Цикл завершается.
Так:
Или вот так:
Таким образом, в приведенном выше коде коллбэк-функция Promise.then
будет выполняться перед коллбэк-функцией второго setTimeout
, потому что это микрозадача, и она была врезана в последовательность задач.
Результат
Задача №10: типичный вопрос с собеседования
Что ж, это наша последняя задача. Если вы сможете правильно определить вывод этого кода, то ваше понимание Promise уже на высоком уровне. И однотипные вопросы на собеседовании точно не станут для вас трудностью.
Что выводит этот фрагмент кода?
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');
Анализ
Эта задача является более суровой версией предыдущей задачи, но основной принцип остается прежним.
Вспомните, что мы узнали ранее:
Синхронный код
Все микрозадачи
Первая макрозадача
Все недавно добавленные микрозадачи
Следующая макрозадача
…
Итак:
Выполним весь синхронный код:
Выполним все микрозадачи:
Выполним первую макрозадачу:
Примечание. На этом шаге макрозадача добавляет в очередь задач новую микрозадачу.
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');;
Заключение
Для всех подобных вопросов нужно просто запомнить три правила:
Интерпретатор JavaScript всегда сначала выполняет синхронный код, а затем асинхронный.
Микрозадачи имеют приоритет над макрозадачами.
3. Микрозадачи могут врезаться в последовательность выполнения в Event Loop.
В марте в рамках специализации FullstackDeveloper пройдут два открытых урока, на которые приглашаем всех желающих.
Тема 1: Прототипное наследование в JavaScript. Разберемся, что такое прототипное наследование и как оно может помочь при разработке программ. В результате вы лучше поймете объектную модель Javascript и сможете писать ООП код с экономией памяти. Регистрация
Тема 2: Какими задачами проверяют ваше знание JavaScript? Посмотрим на типы задач, которые могут дать прямо во время интервью. Обсудим, как и что проверяют эти задачи, и что можно сделать, чтобы подготовиться к таким моментам. Записаться