- Введение в Promises
- Вкратце о том, как работают промисы
- Создание промиса
- Выполнение промиса
- Цепочка промисов
- Обработка ошибок
- Каскадные ошибки
- Оркестровые промисы
- Распространенные ошибки
Введение в Promises
Промисы часто используются, когда мы запрашиваем JSON API
и выполняем AJAX
запросы.
Промисы - это то, что произойдет между настоящим моментом и концом времени; или другими словами это то, что произойдет в будущем, но, вероятно, не сразу. Чтобы понять это, нам нужно помнить, что JavaScript почти полностью асинхронный.
Промисы - это один из способов работы с асинхронным кодом в JavaScript без написания слишком большого количества обратных вызовов.
Хоть они и существовали годами, со временем были стандартизированы и представлены в ES2015, а теперь заменены в ES2017 асинхронными функциями.
Асинхронные функции используют API промисов в качестве своего строительного блока, поэтому их понимание является основополагающим, так как скорее всего, мы больше будем использовать асинхронные функции вместо промисов.
Вкратце о том, как работают промисы
Как только промис был вызван, он запускается в состоянии ожидания (pending state). Это означает, что функция, которая вызвала промис, продолжает выполнение, ожидая, пока промис выполнит свою собственную обработку и предоставит функции вызова некоторую обратную связь.
На этом этапе функция вызова ожидает, пока промис не вернется в resolved state
(выполнено успешно) или в rejected state
(выполнено с ошибкой), и в то же время она продолжает свое выполнение, пока выполняется промис.
Создание промиса
Promise API
предоставляет конструктор Promise
, который мы инициализируем с помощью new Promise()
и он принимает одну функцию которая передает нам resolve
и reject
. Идея заключается в том, что промис либо будет resolve
(выполнен успешно) - завершен и передаст нам данные. Или он может сам себя reject
(отклонить), потому что, возможно, произошла ошибка, или данные были искажены, или по какой-либо другой причине, которая приведет к ошибке. Мы вызываем resolve
или reject
(когда готовы завершить промис) и передаем в них данные для этого промиса.
Например:
let done = true;
const p = new Promise((resolve, reject) => {
if (done) {
resolve("выполнено успешно");
} else {
reject("выполнено с ошибкой");
}
});
В этом примере промис проверяет глобальную константу done
, и если она равна true
- мы возвращаем resolve
(«выполнено успешно»). В противном случае возвращаем rejected
(«выполнено с ошибкой»).
С помощью resolve
и reject
мы можем передать значение. В приведенном выше примере мы просто возвращаем строку, но это может быть и объект.
Выполнение промиса
const p = new Promise((resolve, reject) => {
resolve("Alex is cool");
});
p.then(data => {
console.log(data); // Alex is cool
});
При запуске p
мы выполняем промис и ожидаем успешного выполнения, используя обратный вызов then()
.
Так как мы создали промис, а затем сразу же выполнили resolve
- передав Alex is cool, мы сразу увидем результат в консоле.
Если нужно сделать resolve
через некоторое время
Если мы хотим выполнить некоторую обработку в фоновом режиме или сделать AJAX-запрос, а затем, когда данные вернутся, сделать resolve
. По сути, всё сводится к тому, что «я не хочу останавливать выполнение JavaScript, я просто хочу начать запрос, а затем, когда он вернется, разберусь с этим результатом».
Давай посмотрим, что произойдет, когда мы установим здесь тайм-аут в 1 секунду.
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Alex is cool");
}, 1000);
});
p.then(data => {
console.log(data); // Alex is cool - через 1 секунду
});
Через секунду в консоле появится Alex is cool. Точно так же мы можем вызвать reject
:
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Err Alex is not cool");
}, 1000);
});
p.then(data => {
console.log(data);
});
В консоле увидим Uncaught (in promise) Err Alex is not cool
. Err Alex is not cool
- это настоящая ошибка. Почему Uncaught (in promise)
(не перехвачено промисом)? Потому что мы не словили и не обработали ошибку в промисе. Для этого нам нужно добавить в нашу цепочку catch()
, передать ошибку и вывести ее через console.error
.
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Err Alex is not cool");
}, 1000);
});
p.then(data => {
console.log(data);
}).catch(err => {
console.error(err);
});
Теперь в консоле браузера через 1 секунду будет только реальная ошибка, которую мы передали. В идеале, для более детальной информации, где произошла ошибка, мы должны передать объект Error
, а не просто строку. Поэтому мы обернем строку в объект Error
, а затем, когда ошибка выведется в консоль, мы увидим, что у нас есть информация о том, где она произошла.
reject(Error("Err Alex is not cool"));
Цепочка промисов
Промис можно вернуть другому промису и таким образом создать цепочку промисов.
Отличным примером цепочки промисов является Fetch API
- слой поверх XMLHttpRequest API
. Его мы можем использовать для получения данных, создав цепочку промисов. Они будут выполнятся, когда мы получим данные.
Fetch API
- это механизм, основанный на промисах, а вызов fetch()
- он эквивалентен определению нашего собственного промиса с использованием new Promise()
.
В этом примере мы вызываем fetch()
, чтобы получить данные о моем профиле на github, и создаем цепочку промисов.
const status = response => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response);
}
return Promise.reject(Error(response.statusText));
};
const json = response => response.json();
fetch("https://api.github.com/users/oleksiimyzgin")
.then(status)
.then(json)
.then(data => {
console.log("Request succeeded with JSON response", data);
})
.catch(error => {
console.error("Request failed", error);
});
Запуск fetch()
возвращает response
, который имеет много свойств, из которых мы используем:
status
- числовое значение, предоставляющее код состояния HTTP;statusText
- сообщение о состоянии, котороеOK
, если запрос был выполнен успешно.
У response
также есть метод json()
, который возвращает промис. Выполнив его он содержит нужный нам контент тела, обработанный и преобразованный в JSON.
Таким образом, учитывая эти промисы, вот что происходит: первый промис в цепочке - это определенная нами функция, называемая status()
, которая проверяет статус ответа и, если ответ успешный (между 200 и 299), то выполнится resolve
, в противном случае reject
.
Если выполняется reject
, то цепочка промисов пропустит все перечисленные then()
и перейдет непосредственно к первому catch()
внизу, в котором будет записан текст Request failed
(Ошибка запроса) вместе с сообщением об ошибке.
Если операция прошла успешно то выполняется resolve
, затем вызывается функция json()
, которую мы определили. Поскольку предыдущий промис, в случае успеха, вернул объект response
, мы получаем его в качестве входных данных для второго промиса.
В этом случае мы возвращаем данные, обработанные JSON, поэтому третий промис получает JSON напрямую:
.then((data) => {
console.log('Request succeeded with JSON response', data)
})
это мы и выводим в консоль.
Обработка ошибок
В приведенном примере, в предыдущем разделе, у нас есть catch()
, который был добавлен к цепочке промисов. Когда что-либо в цепочке промисов дает сбой и вызывает ошибку или отклоняет промис, управление переходит к ближайшему catch()
по цепочке.
new Promise((resolve, reject) => {
throw new Error("New Error");
})
.then(res => console.log(res))
.catch(err => {
console.error(err); // Error: New Error
});
// или
new Promise((resolve, reject) => {
reject(Error("New Error"));
})
.then(res => console.log(res))
.catch(err => {
console.error(err); // Error: New Error
});
Каскадные ошибки
Если внутри catch()
мы вызываем ошибку, то можно добавить второй catch()
для её обработки и так далее.
new Promise((resolve, reject) => {
throw new Error("Error 1");
})
.catch(err => {
throw new Error("Error 2");
})
.catch(err => {
console.error(err); // Error: Error 2
});
Оркестровые промисы
Promise.all()
Если нам нужно синхронизировать различные промисы, то Promise.all()
поможет определить список промисов и выполнить что-то только тогда, когда все они будут выполнены успешно.
const weather = new Promise(resolve => {
setTimeout(() => {
resolve({ temp: 25, condition: "Солнечно" });
}, 2000);
});
const person = new Promise(resolve => {
setTimeout(() => {
resolve({ name: "Alex", dev: "Frontend" });
}, 500);
});
Promise.all([weather, person])
.then(res => {
console.log("Array of results", res);
})
.catch(err => {
console.error(err);
});
Синтаксис ES2015 деструктуризации позволяет сделать так:
Promise.all([weather, person]).then(([weatherInfo, personInfo]) => {
console.log("Results", weatherInfo, personInfo);
});
В данном примере мы получим ответ только после 2 секунд, потому что мы ждем, пока каждый промис будет выполнен успешно, прежде чем запустим then
. Иными словами, самый медленный response
будет решать, через сколько вернутся все промисы.
Это был пример с setTimeouts
. Сейчас давай разберем с реальными данными.
Нам нужно 2 API
, с которых мы получим данные. Берём 2 учетные записи с github.
Так как мы получаем в качестве ответа поток данных, мы должны его преобразовать в читаемый json
. Ранее мы делали это с одним ответом через response.json()
. Как это сделать если у нас два response
? Мы перебираем response
с помощью map
, который вернет новый массив и на каждом res
вызываем второй промис json()
.
const data1 = fetch("https://api.github.com/users/oleksiimyzgin");
const data2 = fetch("https://api.github.com/users/leoyats");
Promise.all([data1, data2])
.then(response => {
return Promise.all(response.map(res => res.json()));
})
.then(response => {
console.log(response);
});
Почему мы должны вызывать res.json
?
Причина в том, что существует много разных типов данных, которые могут вернуться. В MDN документации написано что body
может вернуться в виде arrayBuffer
, blob
, json
, text
или formData
. Но не стоит предполагать, что твой API или AJAX запросы всегда будут json
, так как это могут быть данные любого типа, которые там есть.
Promise.race()
Promise.race()
возвращает resolve
или reject
промис, в зависимости от того, с каким результатом завершится первый из переданных ему промисов: со значением или с ошибкой. В данном примере выполнится только самый быстрый промис.
const promiseOne = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Return after 2 seconds");
}, 2000);
});
const promiseTwo = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Return after 1 second");
}, 1000);
});
Promise.race([promiseOne, promiseTwo]).then(result => {
console.log(result); // Return after 1 second
});
Распространенные ошибки
Uncaught TypeError: undefined is not a promise
Если ты получил Uncaught TypeError: undefined is not a promise
в консоли - убедись, что ты используешь new Promise()
, вместо просто Promise()
.