Понимание промисов

Итак, вкратце про промисы: "Представьте, что вы ребенок. Ваша мама обещает вам, что вы получите новый телефон на следующей неделе."

Вы не знаете, получите ли вы его до следующей недели. Ваша мама может купить вам совершенно новый телефон, а может просто этого не сделать, к примеру, потому что она будет не в настроении. Это и есть промис. От английского promise - обещать.

Итак, у промиса есть 3 состояния. Это:

  • Промис в состоянии ожидания (pending). Когда вы не знаете, получите ли вы мобильный телефон к следующей неделе или нет.
  • Промис решен (resolved). Вам реально купят новый телефон.
  • Промис отклонен (rejected). Вы не получили новый мобильный телефон, так как всё-таки, мама была не в настроении.

Создание промиса

Давайте переведем все это в JavaScript.

/_ ES5 _/
var isMomHappy = true;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
			
            resolve(phone);  // Всё выполнено
        } else {
            var reason = new Error('mom is not happy');
            reject(reason);  // reject
        }
    }
);

Код выше довольно выразителен и говорит сам за себя.

1. У нас есть булин isMomHappy, чтобы определить в каком расположении духа мама.

2. У нас есть промис willIGetNewPhone. Этот промис может быть как в состоянии resolved, то есть, если вы получаете мобильный телефон, а также может быть в состоянии rejected, то есть если ваша мама не в настроении и вы не получаете мобильный телефон.

3. Тут у нас стандартный синтаксис для определения нового промиса, как в MDN документации. То есть синтаксис промиса выглядит таким образом.

new Promise(/* Выполняемая функция */ function (resolve, reject) { ... } );

4. Когда вам нужно это запомнить, когда результат успешен, вызывайте resolve (ваше значение при успехе), если результат не успешен, вызывайте reject (ваше значение при неудаче, соответственно). В нашем случае мама в настроении и мы получим телефон. Следовательно, мы вызываем resolve функцию с переменной phone. Если ваша мама не в настроении, мы вызовем функцию reject с reason, то есть reject (reason).


Применяем промисы

Теперь у нас есть промис, давайте применим его, ну или употребим, как хотите.

/_ ES5 _/
...

// Вызываем промис
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // yay, you got a new phone
            console.log(fulfilled);
            // output: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // oops, mom don't buy it
            console.log(error.message);
            // output: 'mom is not happy'
        });
};

askMom();

1. Мы вызываем функцию в askMom. В этой функции, мы применим наш промис willIGetNewPhone.

2. Нам надо сделать одно действие, чтобы промис был решен или отклонен, тут мы будем использовать .then и .catch.

3. В нашем примере, у нас function(fulfilled) {...} в .then. Какое значение у fulfilled? fulfilled значение это точное значение в вашем промисе resolve (значение при успехе). Следовательно, это будет phone.

4. У нас есть function(error) {...} в .catch. Какое значение будет у error? Как вы могли предположить, error значение именно то, которое вы указали в промисе reject (значение при неудаче). Следовательно, в этом случае это будет reason.

Давайте запустим этот пример и увидим результат!


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

Да, в промисах есть цепочки.

Давайте представим, что вы ребенок и обещали своему другу, что покажете ему новый телефон, когда вам его купят. Это будет ещё один промис.

// 2й промис
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

В этом примере вы уже наверное поняли, что мы не вызывали reject. Так как, в принципе, это опционально.

Мы вообще можем сократить этот пример, используя promise.resolve.

// 2й промис
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
        phone.color + ' ' + phone.brand + ' phone';
    
    return Promise.resolve(message);
};

А теперь давайте свяжем наши промисы. Вы - ребенок и можете запустить showOff промис только после промиса willIGetNewPhone.

// Вызываем промис
var askMom = function () {
    willIGetNewPhone
        .then(showOff) // связываем
        .then(function (fulfilled) {
            console.log(fulfilled);
            // output: 'Hey friend, I have a new black Samsung phone.'
        })
        .catch(function (error) {
            // oops, mom don't buy it
            console.log(error.message);
            // output: 'mom is not happy'
        });
};

Вот так легко связывать промисы.


Промисы и асинхронность

Промисы асинхронны. Давайте выведем сообщение перед и после вызовом промиса.

// вызываем наш промис
var askMom = function () {
    console.log('before asking Mom'); // Выводим в консоль до
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // Выводим в консоль после
}

Какова последовательность ожидаемого вывода? Возможно вы ожидали.

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

Но на самом деле вывод будет таким:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

Почему? Потому что жизнь (или JS) никого не ждёт.

Вы, ребенок, не перестали бы играть в ожидании промиса от вашей мамы. Не так ли? Это то, что мы называем асинхронность, код будет запущен без блокирования или ожидания результата. Все что должно подождать промиса перед выполнением, вы вставляете в .then.


Мир до промисов - колбэки

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

// Удаленно прибавляем два числа
// Получаем результат вызывая API

function addAsync (num1, num2, callback) {
    // Используем jQuery getJSON callback API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback
    const result = success; // Получаем 3
});

Синтаксис ок, зачем нам тогда промисы?

Что если вы захотите сделать последующее асинхронное действие?

Давайте представим, что вместо простого сложения чисел единожды, нам надо будет сделать это 3 раза. В обычной функции, мы делаем это:

// Добавляем два числа обычным способом

let resultA, resultB, resultC;

function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // resultA = 3
resultB = add(resultA, 3); // resultB = 6
resultC = add(resultB, 4); // resultC = 10

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

Как это выглядит с колбэками?

// Удаленно добавляется два числа
// Получаем результат вызывая API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // use the famous jQuery getJSON callback API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // Получаем 3

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // Получаем 6

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // Получаем 10

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

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


Побег из колбэк ада

Промисы приходят на помощь. Давайте посмотрим на тот же код, но по версии промисов.

// Складываем два числа удаленно

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // ES6 подтягивает API, который возвращает промис.
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

С промисами, мы выравниваем колбэк с .then. В этом случае, это выглядит чище, так как нет вложенных колбэков. Конечно же с ES7 async синтаксисом, мы можем даже улучшить этот пример, но это уже на ваше усмотрение.