Как работают промисы в JavaScript

Промисы Javascript – не самый простой механизм. Давайте попробуем на простых примерах разобраться, что это такое и как они работают.

Что такое промисы?

С английского promise переводится как «обещание». Для примера рассмотрим такую ситуацию.

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

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

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

  1. На рассмотрении: вы не знаете точно, получите ли вы этот телефон.
  2. Выполнено: мама довольна вами и покупает вам новенький телефон.
  3. Отклонено: мама вами недовольна и не покупает вам телефон.

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

Давайте преобразуем эту ситуацию в JavaScript и попробуем использовать все эти условные данные в написании кода.

// ES5: Part 1

var isMomHappy = false;

// 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
        }

    }
);

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

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

// promise syntax look like this
new Promise(function (resolve, reject) { ... } );

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

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

// ES5: Part 2

var willIGetNewPhone = ... // continue from part 1

// call our promise
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 didn't buy it
            console.log(error.message);
             // output: 'mom is not happy'
        });
};

askMom();

Запустите этот пример и посмотрите на результат.

Примечание: Демо вы найдете здесь.

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

Промисы можно связывать в цепочки.

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

Это еще одно обещание –  еще один промис. Преобразуем его в код:

// ES5

// 2nd promise
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);
        }
    );
};

Обратите внимание: мы можем сократить приведенный выше код следующим образом:

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

    return Promise.resolve(message);
};

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

// call our promise
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // chain it here
    .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'
        });
};

Вот как создаются цепочки промисов.

Асинхронность промисов

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

// call our promise
var askMom = function () {
    console.log('before asking Mom'); // log before
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // log after
}

Какова последовательность ожидаемого результата? Кажется, порядок будет таким:

  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: Full example

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

// call our promise
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // chain it here
    .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'
        });
};

askMom();

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

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

Продемонстрированный здесь код работает в средах ES5 (все основные браузеры + NodeJs), если они поддерживают библиотеку промисов Bluebird. «Из коробки» ES5 не поддерживает промисы. Еще одна известная библиотека – Q от Криса Ковала.

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

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

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

//_ ES6: Full example_

const isMomHappy = true;

// Promise
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);
        }

    }
);

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

// call our promise
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: Full example
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);
        }

    }
);

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

            resolve(message);
        }
    );
};

// call our promise in ES7 async await style
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 it here too
(async () => {
    await askMom();
})();

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

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

Давайте рассмотрим два примера. В каждом из них выполняется сложение двух чисел.

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

// add two numbers normally

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

const result = add(1, 2); // you get result = 3 immediately

Асинхронная функция для сложения двух чисел:

// add two numbers remotely

// get the result by calling an API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// you get result  = "undefined"

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

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

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

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

// add two numbers remotely
// get the result by calling an API

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
    const result = success; // you get result = 3 here
});

Последующее асинхронное действие

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

// add two numbers normally

let resultA, resultB, resultC;

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

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

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

Вот как это выглядит с применением обратных вызовов:

// add two numbers remotely
// get the result by calling an API

let resultA, resultB, resultC;

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

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // you get result = 3 here

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // you get result = 6 here

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // you get result = 10 here

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

Примечание: Демо вы найдете здесь.

Как видите, этот синтаксис менее удобен из-за глубоко вложенных обратных вызовов.

Как избежать глубоко вложенных обратных вызовов?

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

// add two numbers remotely using observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // use ES6 fetch API, which return a promise
    // What is .json()? https://developer.mozilla.org/en-US/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, вы можете сделать этот пример еще лучше.

Что такое Observables?

Observables помогут вам справиться с асинхронными данными.

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

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

function addAsync(num1, num2) {
    // use ES6 fetch API, which return a 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)
  });

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

...

addAsync(1,2)
  .delay(3000) // delay 3 seconds
  .do(x => resultA = x)
  ...

Заключение

Умение работать с обратными вызовами и промисами – очень важный навык. Разберитесь с ними, попробуйте самостоятельно использовать в коде. Observables можно оставить на потом. Эти три элемента JavaScript могут серьезно повлиять на разработку вашего приложения.

Читайте также: Функции async/await в JavaScript

Tags:

Добавить комментарий