Promises в JavaScript: просто о промисах

В этом руководстве вы узнаете о промисах 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 — это объект, который инкапсулирует результат асинхронной операции.

Объект обещания имеет состояние, которое может быть одним из следующих:

  • В ожидании
  • Выполнено со значением
  • Отклонено по причине

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

Состояние «выполнено» указывает на успешное завершение асинхронной операции:

Обещание JavaScript выполнено

Состояние «отклонено» указывает на сбой асинхронной операции.

Состояние "отклонено"

Создание обещания

Чтобы создать объект обещания, вы используете конструктор 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 состояния, оно разрешается.

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() независимо от того, выполнено обещание или отклонено.
Рейтинг
( Пока оценок нет )
Александр Русаков / автор статьи
Программист, разработчик, 12 лет опыта работы в крупных компаниях. Быстро освоил typescript, делюсь своими знаниями на страницах этого сайта.
Загрузка ...
JavaScript и TypeScript