Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

Промисы в JavaScript для чайников

Промисы в JavaScript на самом деле совсем не сложные. Тем не менее, многие люди находят затруднительным их понимание с первых моментов изучения. Поэтому, снизу всё будет описано способом для начинающих, чтобы читающий понял материал полностью и объемно. Помните, что промисы лежат в основе async/await и их понимание, можно сказать, что обязательно для работы с асинхронным JavaScript.

Адаптивный перевод статьи — JavaScript Promises for Dummies

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

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

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

Это и есть промис. От английского promise — обещать. Небольшое уточнение, пожалуйста, не усложняйте понимание другим с произношением, так как во всём русскоязычном мире принято говорить “промис” в случае с JavaScript.

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

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

2. Промис решен (resolved). Вам реально купят новый телефон.

3. Промис отклонен (rejected). Вы не получили новый мобильный телефон, так как всё-таки, мама была не в настроении.

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

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

/_ ES5 _/
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); // 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. Какое значение у fulfilledfulfilled значение это точное значение в вашем промисе 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 .

Промисы в ES5, ES6/2015, ES7/Next

ES 5 — поддерживают почти все браузеры. Демо код работает в ES5 среде (Все основные браузеры + NodeJS), если бы вы подключили библиотеку промисов Bluebird. Почему так? Потому что ES5 не поддерживает промисы из коробки. Другая знаменитая библиотека промисов это Q, от Криса Коваля.

ES6 / ES2015 — демо код сработает прямо из коробки, так как ES6 поддерживает промисы естественным путём. Более того, с ES6 функциями, мы можем ещё круче упростить код с помощью => и использовать const и let.

/_ ES6 _/
const isMomHappy = true;

// Промис
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // fat arrow
        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)) // fat arrow
        .catch(error => console.log(error.message)); // fat arrow
};

askMom();

Обратите внимание, что все var заменены на const. Все function(resolve, reject) были упрощены на (resolve, reject) => .

ES7 — Async/await делают синтаксис визуально лучше. ES7 представил async и await синтаксис. Это делает асинхронный синтаксис визуально лучше и проще для понимания, без .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('mom is not happy');
            reject(reason);
        }

    }
);

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

            resolve(message);
        }
    );
};

// Вызываем промис
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 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, скачивание файлов, чтение файлов — всё это те обычные async операции, которые вы можете выполнять.

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

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

// Удаленно прибавляем два числа
// Получаем результат вызывая 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 синтаксисом, мы можем даже улучшить этот пример, но это уже на ваше усмотрение.

Новичок на районе: Observables

Перед тем как закончить с промисами, есть кое-что, что пришло для того, чтобы облегчить работу с асинхронными данными — это Observables.

Observables — это ленивые потоки событий, которые могут выдать ноль или больше событий, а могут и вообще не закончиться.

Некоторые ключевые различия между промисами и observables:

Они отменяемые

Они ленивы/медленны

Давайте посмотрим на тоже самое демо, только написанное с помощью observables. В этом примере, я использую RxJS.

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

function addAsync(num1, num2) {
    // Используем ES6 fetch API, который возвращает промис
    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.fromPromise конвертит промис в observable поток

.do и .flatMap среди некоторых операторов доступных для Observables

Потоки ленивы. Наш addAsync запускается, когда мы .subscribe на него.

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

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

Заключение

Узнайте про колбэки и промисы. Поймите их и используйте их. Не беспокойтесь о Observables, пока что. Все трое могут повлиять процесс разработки, но всё это будет зависеть от ситуации.