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

Введение

JavaScript Promises могут быть сложными для понимания. Поэтому я хотел бы записать то, как я понимаю промисы.

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

Промис вкратце:

“Представь, что ты ребенок. Твоя мама обещает тебе, что на следующей неделе купит тебе новый телефон.”

Ты не знаешь, получите ли вы этот телефон до следующей недели. Твоя мама может действительно купить тебе новый телефон, или не купить.

Это и есть обещание. Обещание имеет три состояния. Они таковы:

  1. Pending: Ты не знаешь, получите ли вы этот телефон
  2. Fulfilled: Мама довольна, она покупает тебе новый телефон
  3. Rejected: Мама недовольна, она не покупает тебе телефон

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

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

// ES5: Часть 1

var isMomHappy = false;

// 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); // отклонено
        }

    }
);

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

Вот как обычно выглядит синтаксис промиса:

// синтаксис промиса выглядит так
new Promise(function (resolve, reject) { ... } );

Использование промисов

Теперь, когда у нас есть промис, давайте воспользуемся им:

// ES5: Часть 2

var willIGetNewPhone = ... // продолжение с части 1

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

askMom();

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

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

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

Промисы могут быть цепочкой.

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

Это еще одно обещание. Давайте запишем это!

// ES5

// второе обещание
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);
        }
    );
};

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

// сокращаем

// второе обещание
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);
         // вывод: 'Эй, друг, у меня новый черный телефон Samsung.'
        })
        .catch(function (error) {
            // ой, мама не купила
            console.log(error.message);
         // вывод: 'мама не в хорошем настроении'
        });
};

Вот как можно связать промисы.

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

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

// вызов нашего промиса
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.

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

Вот полный пример на ES5:

// ES5: Полный пример

var isMomHappy = true;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // fulfilled
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // reject
        }

    }
);

// 2nd promise
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

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

askMom();

Promises в ES5, ES6/2015, ES7/Next

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

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

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

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

Вот полный пример на ES6:

//_ ES6: Полный пример_

const isMomHappy = true;

// Promise
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // стрелочная функция
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// второй промис
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    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 ввел синтаксис async и await. Он упрощает асинхронный синтаксис, делая его более понятным без использования .then и .catch.

Перепишем наш пример с использованием синтаксиса ES7:

// ES7: Полный пример
const isMomHappy = true;

// Promise
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

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

            resolve(message);
        }
    );
};

// вызов нашего промиса в стиле ES7 async await
async function askMom() {
    try {
        console.log('before asking Mom');

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

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// async await также здесь
(async () => {
    await askMom();
})();

Обещания и когда их использовать

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

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

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

Обычная функция для сложения двух чисел

// обычное сложение двух чисел

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

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

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

function addAsync (num1, num2, callback) {
    // использовать известный колбэк API getJSON jQuery
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // колбэк
    const result = success; // вы получаете result = 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) {
    // использование популярного API колбэков jQuery getJSON
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // колбэк 1
    resultA = success; // здесь вы получаете result = 3

    addAsync(resultA, 3, success => {
        // колбэк 2
        resultB = success; // здесь вы получаете result = 6

        addAsync(resultB, 4, success => {
            // колбэк 3
            resultC = success; // здесь вы получаете result = 10

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

Демо: https://jsbin.com/barimo/edit?html,js,console

Этот синтаксис менее удобен для пользователя из-за глубоко вложенных колбэков.

Избегание глубоко вложенных колбэков

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

// добавить два числа удаленно с использованием observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // использовать API fetch в ES6, который возвращает promise
	// Что такое .json()? https://developer.mozilla.org/ru/docs/Web/API/Body/json
    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 вы можете еще больше улучшить этот пример.

Observable

Прежде чем остановиться на промисах, есть нечто, что поможет вам работать с асинхронными данными, называемое Observable.

Давайте рассмотрим тот же пример, написанный с использованием Observable. В этом примере мы будем использовать RxJS для Observable.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

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

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

Observable могут делать более интересные вещи. Например, с помощью delay можно добавить задержку в 3 секунды всего одной строкой кода или повторить вызов определенное количество раз.

...

addAsync(1,2)
  .delay(3000) // задержка 3 секунды
  .do(x => resultA = x)
  ...

Вы можете прочитать одну из моих статей о RxJs здесь.

Заключение

Ознакомление с колбэками и промисами важно. Понимайте их и используйте. Не беспокойтесь пока об Observable. Все три могут быть полезны в вашем развитии в зависимости от ситуации.

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises