Лабораторные - Промисы


Промис (Обещание)  —  это объект, который содержит будущее значение асинхронной операции. Например, если вы запрашиваете некоторые данные с сервера, промис обещает нам получить эти данные, которые мы сможем использовать в будущем.

Промис в JavaScript имеет 3 состояния. Это может быть:

1) нерешенный (в ожидании) — Промис ожидает, если результат не готов. То есть, ожидает завершение чего-либо (например, завершения асинхронной операции).

2) решенный/resolved (выполненный) — Промис решен, если результат доступен. То есть, что-то завершило свое выполнение (например, асинхронная операция) и все прошло хорошо.

3) отклоненный/rejected — Промиc отклонен, если произошла ошибка в процессе выполнения.

Синтаксис создания Promise:

const promise = new Promise(function(resolve, reject) {
// функция-исполнитель (executor)
});

Функция, переданная в конструкцию new Promise, называется исполнитель (executor). Когда Promise создаётся, она запускается автоматически. Она должна содержать «создающий» код, который когда-нибудь создаст результат.

Её аргументы resolve и reject — это колбэки, которые предоставляет сам JavaScript. Наш код — только внутри исполнителя.

Когда он получает результат, сейчас или позже — не важно, он должен вызвать один из этих колбэков:

Обычно исполнитель делает что-то асинхронное и после этого вызывает resolve/reject, то есть через какое-то время. Но это не обязательно, resolve или reject могут быть вызваны сразу. Например:

const promise = new Promise((resolve, reject) => {
    if (jobDone) {
        resolve('Операция выполнена');
    } else {
        reject('Произошла ошибка');
    }
});

У объекта promise, возвращаемого конструктором new Promise, есть внутренние свойства:

Ниже пример конструктора Promise и простого исполнителя с кодом, дающим результат с задержкой (через setTimeout):

const promise = new Promise(function(resolve, reject) {
    // эта функция выполнится автоматически при вызове new Promise
    // через 1 секунду сигнализировать, что задача выполнена со случайным результатом
    setTimeout(() => resolve(Math.random()), 1000);
});

Исполнитель должен вызвать что-то одно: resolve или reject. Состояние промиса может быть изменено только один раз. Все последующие вызовы resolve и reject будут проигнорированы.

const promise = new Promise((resolve, reject) => {
    resolve('Promise resolved');  // Промис выполнен
    reject('Promise rejected');   // Игнорируется. Промис уже не может быть отклонен
});

Объект Promise служит связующим звеном между исполнителем и функциями-потребителями, которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны) с помощью методов .then, .catch и .finally.

then

Наиболее важный и фундаментальный метод — .then.

Синтаксис:

promise.then(
    function(result) { /* обработает успешное выполнение */ },
    function(error) { /* обработает ошибку */ }
);

Первый аргумент метода .then — функция, которая выполняется, когда промис переходит в состояние «выполнен успешно», и получает результат.

Второй аргумент .then — функция, которая выполняется, когда промис переходит в состояние «выполнен с ошибкой», и получает ошибку.

Например, вот реакция на успешно выполненный промис:

const promise = new Promise(function(resolve, reject) {
    setTimeout(() => resolve("Выполнено"), 1000);
});

// resolve запустит первую функцию, переданную в .then
promise.then(
    result => alert(result), // выведет "Выполнено" через одну секунду
    error => alert(error) // не будет запущена
);

Выполнилась первая функция.

В случае ошибки в промисе выполнится вторая:

const promise = new Promise(function(resolve, reject) {
    setTimeout(() => reject(new Error("Ошибка")), 1000);
});

// reject запустит вторую функцию, переданную в .then
promise.then(
    result => alert(result), // не будет запущена
    error => alert(error) // выведет "Error: Ошибка" спустя одну секунду
);

Если мы заинтересованы только в результате успешного выполнения задачи, то в then можно передать только одну функцию:

const promise = new Promise(resolve => {
    setTimeout(() => resolve("Выполнено"), 1000);
});

promise.then(alert); // выведет "Выполнено" спустя одну секунду

catch

Если мы хотели бы только обработать ошибку, то можно использовать null в качестве первого аргумента: .then(null, errorHandlingFunction). Или можно воспользоваться методом .catch(errorHandlingFunction), который сделает тоже самое:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("Ошибка")), 1000);
});

// .catch(f) это тоже самое, что promise.then(null, f)
promise.catch(alert); // выведет "Error: Ошибка" спустя одну секунду

Вызов .catch(f) — это сокращённый, «укороченный» вариант .then(null, f).

finally

Вызов .finally(f) похож на .then(f, f) в том смысле, что f выполнится в любом случае, когда промис завершится: успешно или с ошибкой.

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

Например:

new Promise((resolve, reject) => {
    /* сделать что-то, что займёт время, и после вызвать resolve/reject */
})
    // выполнится, когда промис завершится, независимо от того, успешно или нет
    .finally(() => остановить индикатор загрузки)
    .then(result => показать результат, err => показать ошибку)

Существует несколько важных отличий от then:

  1. Обработчик, вызываемый из finally, не имеет аргументов. В finally мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача — выполнить «общие» завершающие процедуры.
  2. Обработчик finally «пропускает» результат или ошибку дальше, к последующим обработчикам.

Например, здесь результат проходит через finally к then:

new Promise((resolve, reject) => {
    setTimeout(() => resolve("Данные"), 2000)
})
    .finally(() => alert("Промис завершён"))
    .then(result => alert(result)); // <-- .then обработает результат

А здесь ошибка из промиса проходит через finally к catch:

new Promise((resolve, reject) => {
    throw new Error("Ошибка");
})
    .finally(() => alert("Промис завершён"))
    .catch(err => alert(err));  // <-- .catch обработает объект ошибки

Это очень удобно, потому что finally не предназначен для обработки результата промиса. Так что он просто пропускает его через себя дальше.