В этом руководстве вы узнаете о промисах JavaScript Promises (промисах) и о том, как их эффективно использовать.
В следующем примере определяется функция getUsers()
, которая возвращает список пользовательских объектов:
function getUsers() { return [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }
Каждый пользовательский объект имеет два свойства: username
и email
.
Чтобы найти пользователя по имени из списка пользователей, возвращаемого getUsers()
, вы можете использовать findUser()
следующим образом:
function findUser(username) { const users = getUsers(); const user = users.find((user) => user.username === username); return user; }
В функции findUser()
:
- Во-первых, получите массив пользователей, вызвав
getUsers()
- Во-вторых, найдите пользователя с определенным
username
, используя методfind()
объектаArray
. - В-третьих, верните сопоставленного пользователя.
Ниже показан полный код для поиска пользователя с именем 'john'
:
function getUsers() { return [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; } function findUser(username) { const users = getUsers(); const user = users.find((user) => user.username === username); return user; } console.log(findUser('john'));
{ username: 'john', email: 'john@test.com' }
Код в функции findUser()
является синхронным и блокирующим. Функция findUser()
выполняет функцию getUsers()
для получения массива пользователей, вызывает метод find()
для массива users
для поиска пользователя с определенным именем пользователя и возвращает совпавшего пользователя.
На практике getUsers()
может обращаться к базе данных или вызывать API для получения списка пользователей. Поэтому getUsers()
будет иметь задержку.
Чтобы имитировать задержку, вы можете использовать функцию setTimeout()
. Например:
function getUsers() { let users = []; // delay 1 second (1000ms) setTimeout(() => { users = [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }, 1000); return users; }
Как это работает.
- Сначала определите
users
массива и инициализируйте его значение пустым массивом. - Во-вторых, назначьте массив
users
переменной users внутри обратного вызова функцииsetTimeout()
. - В-третьих, верните массив
users
getUsers()
не будет работать должным образом и всегда возвращает пустой массив. Поэтому findUser()
не будет работать должным образом:
function getUsers() { let users = []; setTimeout(() => { users = [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }, 1000); return users; } function findUser(username) { const users = getUsers(); // A const user = users.find((user) => user.username === username); // B return user; } console.log(findUser('john'));
Выход:
undefined
Поскольку getUsers()
возвращает пустой массив, массив users
пуст (строка A). При вызове метода find()
для массива users
метод возвращает undefined
значение (строка B).
Проблема заключается в том, как получить доступ к users
возвращенным getUsers()
через одну секунду. Один из классических подходов заключается в использовании обратного вызова.
Использование обратных вызовов для работы с асинхронной операцией
В следующем примере аргумент обратного вызова добавляется к getUsers()
и findUser()
:
function getUsers(callback) { setTimeout(() => { callback([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); } function findUser(username, callback) { getUsers((users) => { const user = users.find((user) => user.username === username); callback(user); }); } findUser('john', console.log);
Выход:
{ username: 'john', email: 'john@test.com' }
В этом примере getUsers()
принимает функцию обратного вызова в качестве аргумента и вызывает ее с массивом users
внутри функции setTimeout()
. Кроме того, findUser()
принимает функцию обратного вызова, которая обрабатывает сопоставленного пользователя.
Подход с обратным вызовом работает очень хорошо. Тем не менее, это делает код более трудным для понимания. Кроме того, это усложняет функции с аргументами обратного вызова.
Если количество функций растет, вы можете столкнуться с проблемой ада обратных вызовов. Чтобы решить эту проблему, JavaScript предлагает концепцию промисов.
Понимание промисов
По определению promise — это объект, который инкапсулирует результат асинхронной операции.
Объект обещания имеет состояние, которое может быть одним из следующих:
- В ожидании
- Выполнено со значением
- Отклонено по причине
В начале состояние промиса находится в ожидании, что указывает на то, что асинхронная операция выполняется. В зависимости от результата асинхронной операции состояние меняется либо на выполненное, либо на отклоненное.
Состояние «выполнено» указывает на успешное завершение асинхронной операции:
Состояние «отклонено» указывает на сбой асинхронной операции.
Создание обещания
Чтобы создать объект обещания, вы используете конструктор Promise()
:
const promise = new Promise((resolve, reject) => { // contain an operation // ... // return the state if (success) { resolve(value); } else { reject(error); } });
Конструктор промисов принимает функцию обратного вызова, которая обычно выполняет асинхронную операцию. Эту функцию часто называют исполнителем.
В свою очередь, исполнитель принимает две callback-функции с именами resolve
и reject
.
Обратите внимание, что функции обратного вызова, передаваемые исполнителю, resolve
и reject
только по соглашению.
Если асинхронная операция завершится успешно, исполнитель вызовет функцию resolve()
, чтобы изменить состояние промиса с ожидающего на выполненное со значением.
В случае ошибки исполнитель вызовет функцию reject()
, чтобы изменить состояние промиса с «ожидание» на «отклонено» с указанием причины ошибки.
Как только обещание достигает состояния выполнено или отклонено, оно остается в этом состоянии и не может перейти в другое состояние.
Другими словами, промис не может перейти из состояния fulfilled
в состояние rejected
и наоборот. Кроме того, он не может вернуться из состояния fulfilled
или rejected
в состояние pending
.
После создания нового объекта Promise
его состояние становится ожидающим. Если обещание достигает fulfilled
или rejected
состояния, оно разрешается.
Обратите внимание, что на практике вы редко будете создавать промис-объекты. Вместо этого вы будете потреблять обещания, предоставляемые библиотеками.
Применение промисов: then, catch, finally
1) Метод then()
Чтобы получить значение обещания после его выполнения, вы вызываете метод then()
объекта обещания. Ниже показан синтаксис метода then()
:
promise.then(onFulfilled,onRejected);
Метод then()
принимает две функции обратного вызова: onFulfilled
и onRejected
.
Метод then()
вызывает onFulfilled()
со значением, если обещание выполнено, или onRejected()
с ошибкой, если обещание отклонено.
Обратите внимание, что аргументы onFulfilled
и onRejected
являются необязательными.
В следующем примере показано, как использовать метод then()
объекта Promise
, возвращаемого getUsers()
:
function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); }); } function onFulfilled(users) { console.log(users); } const promise = getUsers(); promise.then(onFulfilled);
Выход:
[ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' } ]
В этом примере:
- Во-первых, определите
onFulfilled()
, которая будет вызываться при выполнении обещания. - Во-вторых, вызовите
getUsers()
, чтобы получить объект обещания. - В-третьих, вызовите метод
then()
объекта обещания и выведите список пользователей на консоль.
Чтобы сделать код более кратким, вы можете использовать стрелочную функцию в качестве аргумента метода then()
следующим образом:
function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); }); } const promise = getUsers(); promise.then((users) => { console.log(users); });
Поскольку getUsers()
возвращает объект промис, вы можете связать вызов функции с методом then()
следующим образом:
// getUsers() function //... getUsers().then((users) => { console.log(users); });
В этом примере getUsers()
всегда завершается успешно. Чтобы имитировать ошибку, мы можем использовать флаг success
, как показано ниже:
let success = true; function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { if (success) { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); } else { reject('Failed to the user list'); } }, 1000); }); } function onFulfilled(users) { console.log(users); } function onRejected(error) { console.log(error); } const promise = getUsers(); promise.then(onFulfilled, onRejected);
Как это работает:
- Сначала определите переменную
success
и присвойте ей значениеtrue
.
Если успех равен true
, промис в функции getUsers()
выполняется со списком пользователей. В противном случае он отклоняется с сообщением об ошибке.
- Во-вторых, определите функции
onFulfilled
иonRejected
. - В-третьих, получите обещание из функции
getUsers()
и вызовите методthen()
сonFulfilled
иonRejected
.
Ниже показано, как использовать стрелочные функции в качестве аргументов метода then()
:
// getUsers() function // ... const promise = getUsers(); promise.then( (users) => console.log, (error) => console.log );
2) Метод catch()
Если вы хотите получить ошибку только тогда, когда состояние обещания отклонено, вы можете использовать метод catch()
объекта Promise
:
promise.catch(onRejected);
Внутри метод catch()
вызывает метод then(undefined, onRejected)
.
В следующем примере флаг success
изменяется на false
, чтобы имитировать сценарий ошибки:
let success = false; function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { if (success) { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); } else { reject('Failed to the user list'); } }, 1000); }); } const promise = getUsers(); promise.catch((error) => { console.log(error); });
3) Метод finally()
Иногда вы хотите выполнить один и тот же фрагмент кода независимо от того, выполнено обещание или отклонено. Например:
const render = () => { //... }; getUsers() .then((users) => { console.log(users); render(); }) .catch((error) => { console.log(error); render(); });
Как видите, вызов функции render()
дублируется как в методах then()
, так и в методах catch()
.
Чтобы удалить этот дубликат и выполнить render()
независимо от того, выполнено обещание или отклонено, вы используете метод finally()
, например:
const render = () => { //... }; getUsers() .then((users) => { console.log(users); }) .catch((error) => { console.log(error); }) .finally(() => { render(); });
Практический пример
В следующем примере показано, как загрузить файл JSON с сервера и отобразить его содержимое на веб-странице.
Предположим, у вас есть следующий файл JSON:
https://www.javascripttutorial.net/sample/promise/api.json
Со следующим содержанием:
{ "message": "JavaScript Promise Demo" }
Ниже показана HTML-страница, содержащая кнопку. При нажатии на кнопку страница загружает данные из файла JSON и показывает сообщение:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>JavaScript Promise Demo</title> <link href="css/style.css" rel="stylesheet"> </head> <body> <div id="container"> <div id="message"></div> <button id="btnGet">Get Message</button> </div> <script src="js/promise-demo.js"> </script> </body> </html>
Ниже показан файл promise-demo.js:
function load(url) { return new Promise(function (resolve, reject) { const request = new XMLHttpRequest(); request.onreadystatechange = function () { if (this.readyState === 4 && this.status == 200) { resolve(this.response); } else { reject(this.status); } }; request.open('GET', url, true); request.send(); }); } const url = 'https://www.javascripttutorial.net/sample/promise/api.json'; const btn = document.querySelector('#btnGet'); const msg = document.querySelector('#message'); btn.addEventListener('click', () => { load(URL) .then((response) => { const result = JSON.parse(response); msg.innerHTML = result.message; }) .catch((error) => { msg.innerHTML = `Error getting the message, HTTP status: ${error}`; }); });
Как это работает.
Сначала определите функцию load()
, которая использует объект XMLHttpRequest
для загрузки файла JSON с сервера:
function load(url) { return new Promise(function (resolve, reject) { const request = new XMLHttpRequest(); request.onreadystatechange = function () { if (this.readyState === 4 && this.status == 200) { resolve(this.response); } else { reject(this.status); } }; request.open('GET', url, true); request.send(); }); }
В исполнителе мы вызываем функцию resolve()
с ответом, если код состояния HTTP равен 200. В противном случае мы вызываем функцию reject()
с кодом состояния HTTP.
Во-вторых, зарегистрируйте прослушиватель событий нажатия кнопки и вызовите метод then()
объекта обещания. Если загрузка прошла успешно, то мы показываем сообщение, возвращенное с сервера. В противном случае показывается сообщение об ошибке с кодом состояния HTTP.
const url = 'https://www.javascripttutorial.net/sample/promise/api.json'; const btn = document.querySelector('#btnGet'); const msg = document.querySelector('#message'); btn.addEventListener('click', () => { load(URL) .then((response) => { const result = JSON.parse(response); msg.innerHTML = result.message; }) .catch((error) => { msg.innerHTML = `Error getting the message, HTTP status: ${error}`; }); });
Заключение
- Обещание ( промис) — это объект, который инкапсулирует результат асинхронной операции.
- Промис начинается в состоянии ожидания и заканчивается либо в состоянии выполнено, либо в состоянии отклонения.
- Используйте метод
then()
, чтобы запланировать выполнение обратного вызова, когда обещание выполнено, и методcatch()
, чтобы запланировать вызов обратного вызова, когда обещание отклонено. - Поместите код, который вы хотите выполнить, в метод
finally()
независимо от того, выполнено обещание или отклонено.