JS

Promise

Promise

Promise (Обещание) - это специальный объект, который предоставляет функционал для организации асинхронного кода.
В нутри самого обещания содержится состояние, которым можно управлять, и которое влияет на развитие дальнейших событий.

состояния:
pending - в самом начале установлено именно такое состояние.
fulfilled - когда промис завершился успешно.
rejected - когда промис завершился с ошибкой.

Promise

На promise можно навешивать коллбэки двух типов:

  • onFulfilled – срабатывают, когда promise в состоянии «выполнен успешно».
  • onRejected – срабатывают, когда promise в состоянии «выполнен с ошибкой».

Promise

pending - как только мы создаем новый промис, то его статус по умолчанию будет таковым, но это не абсолютно во всех случаях. Мы имеем возможность создать промис уже с установленным состоянием выполнено или отклонено. Такое делается для тех случаев когда мы уже имеем результирующие данные, но все равно хотим на их основе постоить асинхронный код.

fulfilled - состояние свидетельствующее о правильном завершении выполнения асинхронного кода. Устанавливается самим асинхронным кодом.

rejected - состояние свидетельствующее об ошибке выполнения асинхронного кода. Устанавливается через соответствующий вызов в асинхронном коде, или выбрасывание ошибки.

Promise

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

let promise = new Promise(function(resolve, reject) {
    // тут будет код, который будет вызван автоматически при создании промиса

    // тут могут быть любые асинхронные вызовы, запорсы к серверу, таймеры.
});
  • onFulfilledфункция, которая будет вызвана с результатом при resolve.
  • onRejectedфункция, которая будет вызвана с ошибкой при reject.
function onFulfilled(data) {
  console.log('success flow', data);
}

function onRejected(data) {
  console.log('error flow', data);
}

promise.then(onFulfilled, onRejected)

Promise

С его помощью можно назначить как оба обработчика сразу, так и только один:

 
// onFulfilled сработает при успешном выполнении
promise.then(onFulfilled)
// onRejected сработает при ошибке
promise.then(null, onRejected)

Promise

reject - можно так же вызвать выбрасыванием исключения (ошибки), через throw new Error.

var someData = false;

var promise = new Promise(function(resolve, reject) {
    if (!someData) {
        throw new Error('нет данных');
    }
});

Promise

Для того, чтобы поставить обработчик только на ошибку, вместо .then(null, onRejected) можно написать .catch(onRejected) – это то же самое.

let p = new Promise((resolve, reject) => {
    // то же что reject(new Error("o_O"))
    throw new Error("o_O");
})

p
.then(function() {
    //some code
})
.catch(function(error) {
    console.log(error);
}); // Error: o_O

Promise

Если в функции промиса происходит синхронный throw (или иная ошибка), то вызывается reject:

let p = new Promise((resolve, reject) => {
    // то же что reject(new Error("o_O"))
    throw new Error("o_O");
})

p.catch(function(error) {
    console.log(error);
}); // Error: o_O

Promise

Асинхронный пример с setTimeout (пример со стрелочными функциями)

let promise = new Promise((resolve, reject) => {

  setTimeout(() => {
    // переведёт промис в состояние fulfilled с результатом "result"
    resolve("result");
  }, 1000);

});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    (result) => {
      // первая функция-обработчик - запустится при вызове resolve
      console.log("Fulfilled: " + result); // result - аргумент resolve
    },
    (error) => {
      // вторая функция - запустится при вызове reject
      console.log("Rejected: " + error); // error - аргумент reject
    }
  );

Promise

Асинхронный пример с setTimeout.

let promise = new Promise((resolve, reject) => {

  setTimeout(() => {
    // переведёт промис в состояние fulfilled с результатом "result"
    resolve("result");
  }, 1000);

});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    function (result) {
      // первая функция-обработчик - запустится при вызове resolve
      console.log("Fulfilled: " + result); // result - аргумент resolve
    },
    function (error) {
      // вторая функция - запустится при вызове reject
      console.log("Rejected: " + error); // error - аргумент reject
    }
  );

Task

Что нужно сделать что бы код который ниже вызвал функцию для обработки ошибки и вывел Rejected ...?

let promise = new Promise((resolve, reject) => {

  setTimeout(() => {
    // переведёт промис в состояние fulfilled с результатом "result"
    resolve("result");
  }, 1000);

});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    function (result) {
      // первая функция-обработчик - запустится при вызове resolve
      console.log("Fulfilled: " + result); // result - аргумент resolve
    },
    function (error) {
      // вторая функция - запустится при вызове reject
      console.log("Rejected: " + error); // error - аргумент reject
    }
  );
let promise = new Promise((resolve, reject) => {

  setTimeout(() => {
    // переведёт промис в состояние fulfilled с результатом "result"
    reject(new Error("время вышло!"));
  }, 1000);

});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    function (result) {
      // первая функция-обработчик - запустится при вызове resolve
      console.log("Fulfilled: " + result); // result - аргумент resolve
    },
    function (error) {
      // вторая функция - запустится при вызове reject
      console.log("Rejected: " + error); // error - аргумент reject
    }
  );

Promise

Call Back

Обработчик или колбэк (call back), это функция которая будет вызвана при переводе промиса в определенное состояние resolved или rejected.

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

Call Back

function onResolved() {
    console.log('Promise выполнился успешно');
}
function onRejected() {
    console.log('Promise завершился ошибкой');
}

var promise = new Promise(function(resolve, reject) {
    // асинхронный код
});

promise.then(onResolved, onRejected);

Если в асинхронном коде будет вызвана функция resolve то обработчик onResolved будет вызван, или же если будет вызвана фуникця reject или будет выброшено исключение, то будет автоматически вызвана фуникця onRejected.

Но если выбрасывать исключение, то оно обязательно должно быть синхронным в асинхронном коде.

Call Back

function onResolved() {
    console.log('Promise выполнился успешно');
}
function onRejected() {
    console.log('Promise завершился ошибкой');
}

var promise = new Promise(function(resolve, reject) {
    // асинхронный код
});

promise
  .then(onResolved)
  .catch(onRejected);

then может принимать два колбека для success или error. Или можем использовать цепочку вызовов через then и catch.

Call Back

function onResolved() {
    console.log('Promise выполнился успешно');
}
function onRejected() {
    console.log('Promise завершился ошибкой');
}

var someData = false;
var promise = new Promise(function(resolve, reject) {
    if (!someData) {
        throw new Error('нет данных'); // это синхронный выброс ошибки
    }

    setTimeout(function() {
        // это не является синхронным выбросом
        throw new Error('что то случилось');
    }, 1000);
});

promise.then(onResolved, onRejected);

Что значит синхронный в асинхронном коде, это означает что вызов чего то будет происходить не во внутренних функциях, а лишь только в самой асинхронной функции.

Call Back

function onResolved(result) {
    console.log(result);
    console.log('Promise выполнился успешно');
}
function onRejected(error) {
    console.log(error);
    console.log('Promise завершился ошибкой');
}

var someData = false;
var promise = new Promise(function(resolve, reject) {
    if (!someData) {
        throw new Error('нет данных'); // это синхронный выброс ошибки
    }
});

promise.then(onResolved, onRejected);

Правильный выход при помощи throw new Error(err)

Call Back

function onResolved(result) {
    console.log(result);
    console.log('Promise выполнился успешно');
}
function onRejected(error) {
    console.log(error);
    console.log('Promise завершился ошибкой');
}

var someData = false;
var promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject(new Error('что то случилось'));
    }, 1000);
});

promise.then(onResolved, onRejected);

Правильный выход при помощи reject в асинхронном коде

Call Back

Важная особенность, когда состояние промиса меняется на fulfilled или rejected то изменить его уже невозможно. Данные которые будут переданы в фуникцию resolve или reject, будут сохранены в промисе, и переданы всем колбэкам которые подписаны на данный результат.

Call Back

function onResolved() {
    console.log('Promise выполнился успешно');
}
function onRejected() {
    console.log('Promise завершился ошибкой');
}

var someData = false;
var promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('Выполнение кода завершилось успешно');
    }, 1000);

    setTimeout(function() {
        reject(new Error('возникла какая то ошибка');
    }, 2000);
});

promise.then(onResolved, onRejected);

После того как первый таймер вызовет resolve, то вызов второго таймера ни к чему не приведет, и вызов reject будет проигнорирован.

Промосификация

function httpGet(url) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();

        xhr.open('get', url, true);
        xhr.onload = function() {
            if (this.status == 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        };
        xhr.onerror = function() { 
          reject(new Error("Network Error")); 
        };
        xhr.send();
    });
}

Промисификация - это когда для асинхронного кода создают обертку которая возвращает промис, для того что бы с ним было проще работать, и делать однотипные задачи более быстрым способом.

Промосификация

httpGet("/user.json")
    .then(
        function(response) {
            console.log('Success: ' + response);
        },
        function(error) {
            console.log('Error: ' + error);
        }
    );

Промосификация

var getJSON = function(url) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        var responseType = 'responseType' in xhr;

        xhr.open('get', url, true);

        xhr.responseType = 'json';

        xhr.onload = function() {
            var status = xhr.status;
            var data;

            if (status === 200) {
                data = responseType ? xhr.response : JSON.parse(xhr.responseText);

                resolve(data);
            } else {
                reject(status);
            }
        };
        xhr.send();
    });
};

getJSON('data.json').then(function(data) {
    console.log('Success ', data);
}, function(status) {
    console.log('Something went wrong.', status);
});

Цепочки промисов

Promise поддерживают цепочки вызовов (Chaining). Это одна из главных особенностей почему используют promise.

Например, мы хотим по очереди:

  1. Загрузить данные посетителя с сервера (асинхронно).
  2. Затем получить данные с github (асинхронно).
  3. Когда это будет готово, вывести его github-аватар на экран (асинхронно).
  4. …И сделать код расширяемым, чтобы цепочку можно было легко продолжить.

Цепочки промисов

httpGet('/user.json')
    .then(function(response) {
        return JSON.parse(response); // приводим строку к обьекту
    })
    .then(function(user) {
        return httpGet('https://api.github.com/users/' + user.githubName);
    })
    .then(function(response) {
        return JSON.parse(response);
    })
    .then(function(githubUser) {
        let image = document.createElement('img');

        image.src = githubUser.avatar_url;
        document.body.appendChild(img);
        
        setTimeout(function() {
            image.remove();
        }, 3000);
    });

Цепочки промисов

При таком чейнинге, результат предидущего выполнения передается в последующий колбэк, но за одним отличием. Если результатом предидущего выполнения является промис, то в следующий вызов колбэка будет передан не сам промис, а результат его выполнения. То есть результат функции resolve, но это скрыто под капотом и нам нет надобности вызывать самим эти функции.

Перехват ошибок

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

А что, если github не отвечает? Или JSON.parse бросил синтаксическую ошибку при обработке данных?

Да мало ли, где ошибка…

Правило здесь очень простое.

 

При возникновении ошибки – она отправляется в ближайший обработчик onRejected.

Перехват ошибок

При возникновении ошибки обработчик нужно поставить через второй аргумент .then(..., onRejected) или, что то же самое, через .catch(onRejected).

httpGet('/page-not-found')
    .then(JSON.parse)
    .then(function(user) {
        return httpGet('https://api.github.com/users/' + user.githubName);
    })
    .then(JSON.parse)
    .then(function(githubUser) {
        var image = document.createElement('img');
        image.src = githubUser.avatar_url;
        document.body.appendChild(img);

        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                image.remove();
                resolve();
            }, 3000);
        });
    })
    .catch(function(error) {
        console.log(error);
    });

Перехват ошибок

Принцип очень похож на обычный try..catch: мы делаем асинхронную цепочку из .then, а затем, когда нужно перехватить ошибки, вызываем .catch(onRejected).

Параллельное выполнение

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

Параллельное выполнение

Promise.all( itera ) - этот статический метод получает на вход массив промисов, и дожидается выполнения каждого, результатом этого метода является промис, который ничем не отличается от остальных, за исключением того что его результат зависит от результатов указанных промисов.
В случае если один из указанных промисов выдаст ошибку, то результирующий промис перейдет в состояние rejected, и вызовет соответствующий колбэк, но аргументом этого колбэка будет ошибка именно того промиса который ее выдал, а результаты остальных промисов будут проигнорированы.
В случае успешного завершения всех промисов, мы получим в
onResolved в результирующем колбэке массив всех результатов каждого промиса.

Параллельное выполнение

Promise.all([
  httpGet('/article/promise/user.json'),
  httpGet('/article/promise/guest.json')
]).then(results => {
  alert(results);
});

Параллельное выполнение

Promise.all([
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(1);
        }, 1000);
    }),
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(2);
        }, 2000);
    }),
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(3);
        }, 3000);
    })
]).then(
    function (results) {
        console.log(results);
    },
    function (error) {
        console.log(error);
    }
);

Параллельное выполнение

Promise.all([
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(1);
        }, 1000);
    }),
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(2);
        }, 2000);
    }),
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            reject(new Error('error'));
        }, 3000);
    })
]).then(
    function (results) {
        console.log(results);
    },
    function (error) {
        console.log(error);
    }
);

with error reject(new Error('error'))

Параллельное выполнение

Promise.race( iterable ) - этот метод почти аналогичен предидущему Promise.all() но за исключением того что результат будет только первого выполнившегося промиса. А все последующие промисы и их результаты будут проигнорированы.

Promise.resolve( value ) - этот метод создает уже успешно выполненый промис, с результатом указанным в аргументе как value.

Promise.reject( value ) - этот метод создает промис завершенный с ошибкой, которую мы так же передаем в аргумент метода.

Deffered

Deferred объект это альтернатива нативному промису, за исключением того что этот объект кастомный, и имеет свой специфичный набор фуникций. Но на практике в работе он аналогичен обычному промису.

Методы деферед объекта немного отличаются по названиям, но по сути аналогичны методам из промисов.

Deffered

Deferred.done( handler ) - аналогичен методу промиса Promise.then(handler, null);

Deferred.fail( handler ) - аналогичен методу промиса Promise.then(null, handler);

Deferred.then( handlerDone , handlerFail ) - аналогичен методу промиса Promise.then(handlerDone , handlerFail);

Так же у этого метода есть 3 аргумент, в отличии от промиса, он не является always колбэком, этот аргумент является колбэком на промежуточные статусы выполнения, которые тригирятся при помощи методов notify и notifyWith. Их мы рассмотрим далее.

Deferred.always( handler ) - аналогичен методу промиса Promise.then(null, null, always); и вызывается при любом изменении состояние деферед объекта.

Deffered

Deferred.progress( handler ) - этот метод регистрирует обработчики которые отслеживают промежуточный результат, аналог вызова
Deferred.then(null, null, handler).

Deferred.resolve( args ) - этот метод меняет состояние дефереда успешно выполнено, и вызывает установленные обработчики на состояние resolved.

Deferred.reject( args ) - этот метод меняет состояние дефереда на выполнено с ошибкой, и вызывает установленные обработчики на состояние rejected.

Deferred.resolveWith( context,  args ) - этот метод аналогичен методу resolve, но за исключением того что первым аргументом можно отдать контекст для обработчиков.

Deferred.rejectWith( context,  args ) - этот метод аналогичен методу reject, и как метод resolveWith так же позволяет задавать контекст для обработчиков, только меняя при этом статус дефереда на выполнено с ошибкой.

Deffered

Deferred.notify( args ) - этот метод позволяет вызывать обработчики промежуточного состояния, и метод можно вызывать сколько угодно раз, но лишь до тех пор пока деферед не изменил свое состояние на выполнено.

Deferred.notifyWith( context, args ) - этот метод аналогичен методам resolveWith и rejectWith позволяя указывать контекст своим обработчикам. Но выполнить его можно как и notify, до изменения состояния.

Deferred.state() - этот метод возвращает состояние дефереда,
'pending' - деферед еще не выполнен

'resolved' - деферед перешел в состоянтие успешно выполнено

'rejected' - деферед перешел в состояние выполнено с ошибкой

$.when( deferredsArray ) - этот метод аналогичен методу Promise.all, создавая деферед объект который ожидает выполнения всех дефередов переданных методу when.

async / await

Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.

async function f() {
  return 1;
}
async function f() {
  return 1;
}

f().then(alert);

await

// работает только внутри async–функций
let value = await promise;

В этом примере промис успешно выполнится через 1 секунду:

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("готово!"), 1000)
  });

  let result = await promise; // будет ждать, пока промис не выполнится (*)

  alert(result); // "готово!"
}

f();

await нельзя использовать в обычных функциях

Обработка ошибок

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
    Promise.reject();
  } catch(err) {
    // перехватит любую ошибку в блоке try: и в fetch, и в response.json
    console.log(err);
  }
}

f();

Итого

Ключевое слово async перед объявлением функции:

  1. Обязывает её всегда возвращать промис.
  2. Позволяет использовать await в теле этой функции.

Ключевое слово await перед промисом заставит JavaScript дождаться его выполнения, после чего:

  1. Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось throw.
  2. Иначе вернётся результат промиса.

Вместе они предоставляют отличный каркас для написания асинхронного кода. Такой код легко и писать, и читать.

Хотя при работе с async/await можно обходиться без promise.then/catch, иногда всё-таки приходится использовать эти методы (на верхнем уровне вложенности, например). Также await отлично работает в сочетании с Promise.all, если необходимо выполнить несколько задач параллельно.

Links

JS

By Oleg Rovenskyi