Обещания JavaScript для чайников

Обещания JavaScript для чайников

JavaScript обещания не сложные. Тем не менее, много людей находят их тяжелыми для понимания. Поэтому хочу написать способ к их пониманию.

Понимание обещаний

Вкратце, обещание это:

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

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

Это обещание. У обещания есть три состояния:

  1. Обещание ожидается. Вы не знаете, получите новый телефон на следующей неделе или нет.
  2. Обещание разрешается. Ваша мама действительно покупает новый телефон.
  3. Обещание отклоняется. Вы не получаете новый телефон.

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

Переведем это в JavaScript.

/* ES5 */
var isMomHappy = false;

// Обещание
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // Разрешается 
        } else {
            var reason = new Error('Мама не довольна');
            reject(reason); // Отклоняется
        }

    }
);

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

  1. У нас есть логический isMomHappy, чтобы обозначить довольна мама или нет.
  2. У нас есть обещание willIGetNewPhone. Обещание может быть разрешено (если мама покупает новый телефон) или отклонено (мама не довольна, и не покупает телефон).
  3. Есть стандартный синтаксис, чтобы определить обещание, согласно документации MDN, он выглядит так:
    // Синтаксис обещаний выглядит так
    new Promise( function (resolve, reject) { ... } );
    
  4. Вот что стоит запомнить, когда результат успешен, вызывается resolve(success_value), если результат не удался, вызывается reject(fail_value) в вашем обещании. В нашем примере, если мама счастлива, мы получим телефон. Следовательно, мы вызываем resolve функцию с переменной phone. Если мама несчастлива, мы вызываем функцию reject с причиной reject(reason);.

Использование обещаний

Итак у нас есть обещания, давайте их использовать.

/* ES5 */

// Вызываем обещание
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // получили новый телефон
            console.log(fulfilled);
         // Вывод: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // упс, мама его не купила
            console.log(error.message);
         // Вывод: 'Мама не довольна'
        });
};

askMom();
  1. Мы вызываем функцию askMom. В функции мы используем наше общение willIGetNewPhone.
  2. Мы добавим некое действие если наше обещание будет разрешено или отклонено, мы используем .then и .catch, чтобы поймать наше действие.
  3. В нашем примере, у нас есть function(fulfilled) {} в .then. Что за переменная fulfilled? Значение fulfilled, именно то значение которое прошло в обещании resolve(success_value). В нашем случае, это будет reason.

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

Демо: https://jsbin.com/nifocu/1/edit?js,console

Пример обещаний javascript

Цепочка обещаний

Обещания можно объединять в цепочки.

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

Это другое обещание, напишем его!

// 2-е Обещание
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Привет друг, у меня есть новый ' +
                phone.color + ' телефон ' + phone.brand;

            resolve(message);
        }
    );
};

Заметки

  • В этом примере, как вы могли заметить, мы не вызываем reject. Это опционально.
  • Мы можем уменьшить пример используя Promise.resolve.
// Укоротим
...

// 2-е Обещание
var showOff = function (phone) {
    var message = 'Привет друг, у меня есть новый ' +
                phone.color + ' телефон ' + phone.brand;

    return Promise.resolve(message);
};

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

// Вызываем наше обещание
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // связываем их
    .then(function (fulfilled) {
            console.log(fulfilled);
         // Вывод: 'Привет друг, у меня есть новый черный Samsung телефон.'
        })
        .catch(function (error) {
            console.log(error.message);
         // Вывод: 'Мама не довольна'
        });
};

Вот так просто можно создавать цепочки обещаний.

Обещания асинхронны

Обещания асинхронны. Запишем лог сообщений до и после вызова обещания.

// Вызываем
var askMom = function () {
    console.log('До просьбы к маме');
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('После просьбы');
}

Какова ожидается последовательность вывода? Наверное вы ждете такой:

  1. До просьбы к маме
  2. Привет друг, у меня новый черный телефон Samsung
  3. После просьбы

Однако, на самом деле порядок такой:

  1. До просьбы к маме
  2. После просьбы
  3. Привет друг, у меня новый черный телефон Samsung
Пример асинхронности обещаний

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

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

Обещания в ES5, ES6/2015, ES7

ES5 - большинство браузеров

Код демо работает в окружении ES5 (большинство браузеров и NodeJs) если вы подключите библиотеку Bluebird. Это необходимо, потому что ES5 не поддерживает обещание из коробки. Другая известная библиотека для обещаний это Q от Kris Kowal.

ES6 / ES2015 - новые браузеры, NodeJs v6

Демо код работает из коробки потому что ES6 нативно поддерживает обещания. В дополнение, с ES6 функцией, мы можем упростить код с => и используя const и let.

Вот пример кода ES6:

/* ES6 */
const isMomHappy = true;

// Обещание
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Мама не довольна');
            reject(reason);
        }
    }
);

const showOff = function (phone) {
    const message = 'Привет друг, у меня есть новый ' +
                phone.color + ' телефон ' + phone.brand;
    return Promise.resolve(message);
};

// Вызываем наше обещание
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled))
        .catch(error => console.log(error.message));
};

askMom();

Заметьте, что все var мы заменили const. Все function(resolve, reject) были упрощены до (resolve, reject) =>. В этих изменениях есть свои преимущества. Подробнее читайте здесь:

ES7 - Async Await уменьшают синтаксис

ES7 представил синтаксис await и async. Это уменьшает синтаксис и делает его легче для понимания, без .then и .catch.

Перепишем наш пример на ES7.

/* ES7 */
const isMomHappy = true;

// Обещание
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('Мама не довольна');
            reject(reason);
        }

    }
);

// 2-у обещание
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Привет друг, у меня есть новый ' +
                phone.color + ' телефон ' + phone.brand;

            resolve(message);
        }
    );
};

// Вызываем наше обещание
async function askMom() {
    try {
        console.log('До просьбы к маме');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('После просьбы');
    }
    catch (error) {
        console.log(error.message);
    }
}

(async () => {
    await askMom();
})();
  1. Всякий раз когда вам нужно вернуть обещание в функцию, вы дописываете перед именем к функции async. Т.е. async function showOff(phone).
  2. А когда вам нужно вызвать обещание, добавьте await. И.е. let phone = await willIGetNewPhone; и let message = await showOff(phone);.
  3. Используйте try { … } catch(error) { … } чтобы поймать ошибку, отклоненного обещания.

Почему обещания и когда их использовать?

Зачем нам нужны обещания? Как выглядел мир до них? Перед тем как ответить на эти вопросы нам нужно вернуться к основам.

Обычные функции против асинхронных

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

Обычная функция

// Сложим два числа

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

const result = add(1, 2); // Вы получите результат = 3 мгновенно

Асинхронная функция

// сложим два числа удаленно

// Получим результат вызывая API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// Вы получите результат = "undefined"

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

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

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

Мир до обещаний: функции обратного вызова (callback)

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

// Сложим два числа удаленно
// получим результат вызывая 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
});

Синтаксис выглядит нормально, так зачем нам нужны обещания?

Что если вы хотите выполнить вложенное асинхронное действие

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

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);

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


let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    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 here

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // вы получите результат = 10 here

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

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

Спасение из ада колбэков

Чтобы нас спасти, пришли обещания. Взглянем на этот же пример, но с обещаниями.

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // используем ES6 fetch 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('Итого: ' + success)
        console.log(resultA, resultB, resultC)
    });

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