Promise и промисификация

Jul 29, 2018

Объект Promise является абстракцией, позволяющей функциям возвращать объект Promise, представляющий конечный результат асинхронной операции.

Состояние объекта Promise

  • Fulfilled (выполнено)
  • Pending (состояние ожидания данных)
  • Rejected (отклонено)

then

Метод then объекта Promise используется для того, чтобы получить результат выполнения или ошибку:

promise.then([onFulfiled], [onRejected])

Метод then синхронно возвращает другой объект Promise.
Если любая функция (onFulfiled или onRejected) вернет значение data, то метод then вернет один из следующих объектов Promise:

  • выполненный со значением data, если data является значением;
  • выполненный с объектом data, где data является объектом Promise или thenable-объектом*;
  • отклоненный с причиной отклонения data, где data является объектом Promise или thenable-объектом

Так как методы Promise.prototype.then и Promise.prototype.catch сами возвращают обещания, их можно вызывать цепочкой, создавая соединения. Именно поэтому Promise позволяют делать цепочки обработки из нескольких синхронных операций.

// fetch - встроенный метод для ajax-запросов (возвращает promise)
fetch('https://api.github.com/users/user')
    .then(function(response){
        if(response.status === 200){
            return response;
        }
        throw new Error(response.status);
    })
    .then(function(response){
        return response.json();
    })
    .then(function(data){
        console.log(data.name);
    })
    .catch(function(error){
        console.log(error.stack);
    })

throw и catch

Если в onFulfiled или onRejected выбросить исключение при помощи оператора throw, объект Promise возвращаемый методом then будет отклонен. Исключение передастся по цепочке, а это большое преимущество по сравнению с асинхронностью основанной на callback-ах.

В цепочке исключение обрабатывается в ближайшем catch().

fetch('https://api.github.com/users/user')
    .then(function(response){
        // throw new Error(response.status); // или
        throw 'error';
    })
    .then(function(response){
        return response.json();
    })
    .then(function(data){
        console.log(data.name);
    })
    .catch(function(error){
        // console.log(error.stack);
    	console.log(error);
    })

*thenable-объект - это Promise-подобный объект, который имеет метод then(); при это реализация такого объекта отличается от реализации Promise.

Статические методы объекта Promise

Promise.resolve

Promise.resolve(value) - возвращает новый объект Promise, созданный из thenable-объекта, если value thenable-объект, или возвращает Promise выполненный с переданным значением, если value - значение;

// Выполнение с thenable объектом
var p1 = Promise.resolve({
    then: function(onFulfill, onReject) {
        onFulfill("fulfilled!");
    }
});
console.log(p1 instanceof Promise) // true

p1.then(function(v) {
    console.log(v); // "fulfilled!"
  }, function(e) {
    // не вызывается
});

Promise.all

Promise.all([promise1, promise2, ...]) - возвращает Promise, который выполнится тогда, когда будут выполнены все обещания, переданные в виде перечисляемого аргумента, или отклонено любое из переданных обещаний.

Promise.race

Promise.race([promise1, promise2, ...]) - возвращает выполненный или отклоненный Promise, в зависимости от того, с каким результатом завершится первое из переданных обещаний, со значением или причиной отклонения этого обещания.

Методы экземпляра Promise

  • promise.then(onFulfiled, onRejected)
  • promise.catch(onRejected)

Промисификация

Промисификация - это процесс преобразования функций, основанный на cb, в функции, которые возвращают объект Promise.

Соглашение для обратных вызовов в Node.js разрешает промисификацию.

Кастомная промисификация

Промисифиция функции - ф-я выполняет какую-либо асинхронную операцию; последний параметр в ф-и это cb, который будет вызван с cb(new Error('Invalid')); в случае ошибки и как cb(null, params); в случае успеха.

// module.exports = function(callbackBasedApi) { // если в разных файлах
    const promisify = function(callbackBasedApi) {
    return function promisified() {
        const args = [].slice.call(arguments);

        console.log('args: ', args); // [ 10, 2 ]

        return new Promise((resolve, reject) => {
            args.push((err, result) => {  // [1]
                if (err) {
                    return reject(err);
                }
                if (arguments.length <= 2) {
                    resolve(result);
                } else {
                    resolve([].slice.call(arguments, 1));
                }
            });
            callbackBasedApi.apply(null, args);
        });
    }
};

// const promisify = require('./promisify.js'); // если в разных файлах
// Функция для промисификации
const delayedDivision = (div, divis, cb) => {
    setTimeout(() => {
        if (typeof div !== 'number' || typeof divis !== 'number' || divis === 0) {
            cb(new Error('Invalid'));
        }

        cb(null, div/ divis);
    }, 1000);
};


const promisifiedDivision = promisify(delayedDivision);

promisifiedDivision(10, 2)
    .then((value) => console.log(value))    // 5
    .catch((error) => console.log(error));
http://jsfiddle.net/dnzl/7dfr6gzq/9/embed/js/dark/

[1] Так как cb всегда передается в последнем аргументе, то мы push ее в список аргументов args.

stackoverflow.com: * Зачем передавать null apply или call.

Промисификация с util.promisify

util.promisify - функция, которая занимается превращением обычной функции, с cb в качестве последнего аргумента, в функцию которая возвращает Promise. Работает в Node c 8 версии.

const promisify = require('util').promisify;
// http://2ality.com/2017/05/util-promisify.html
const promisify = require('util.promisify');

const delayedDivision = (div, divis, cb) => {
    setTimeout(() => {
        if (typeof div !== 'number' || typeof divis !== 'number' || divis === 0) {
            cb(new Error('Invalid'));
        }

        cb(null, div/ divis);
    }, 1000);
};
const promisifiedDivision = promisify(delayedDivision);
promisifiedDivision(10, 2)
    .then((value) => console.log(value))        //5
    .catch((error) => console.log(error));

   Промисифицируем fs.readFile

// echo.js

const {promisify} = require('util');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile); // (A)

const filePath = process.argv[2];

readFileAsync(filePath, {encoding: 'utf8'})
    .then((text) => {
        console.log('CONTENT:', text);
    })
    .catch((err) => {
        console.log('ERROR:', err);
    });

Исходник взят с сайта 2ality.com :    Node.js 8: util.promisify()

Плюсы от использования Promise

  • Применение Promise делают код простым и наглядным.
  • В промисифицированном коде отсутствует логика передачи ошибок, которая требовалась при использовании cb.

Примеры (с сайта dnzl.ru)

Обертка над XMLHttpRequest

Создаем экземпляр Promise, но не передаем какие-либо параметры, так как используем конструктор напрямую:

// Такой promises выгдялет как обертка над некоторой асинхронной логикой
var promise = new Promise(function(resolve,reject){
    var xhr = new XMLHttRequest();
    xhr.open(method, url);
    xhr.onload = function(){  // в момент, когда получаем данные подаем данные на код resolve
        resolve(xhr.response);
    }
    xhr.onerror = function(){ // если error подаем данные в rejecte
        reject(new Error('network request failed'));
    }
    xhr.send();
});

promise.then(function(response){
    // обработка результата
});
promise.catch(function(error){
    //обработка ошибки
})

Создаем и возвращаем экземпляр Promise посредством функции, которая принимает url в качестве параметра:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

// Использование
get('story.json').then(function(response) {
    console.log("Success!", response);
}, function(error) {
    console.error("Failed!", error);
});

Promise и setTimeout

'use strict';

// Создаётся объект promise
let promise = new Promise(function (resolve, reject)  {

  setTimeout(function() {
    // переведёт промис в состояние fulfilled с результатом "result"
    resolve("result");
  }, 1000);

});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    function result(result) {
      // первая функция-обработчик - запустится при вызове resolve
      alert("Fulfilled: " + result); // result - аргумент resolve
    },
    function result(error) {
      // вторая функция - запустится при вызове reject
      alert("Rejected: " + error); // error - аргумент reject
    }
  );

// через 1 секунду выведется «Fulfilled: result»

Применение Promise на практике

// PROMISE:

function applyForVisa(documents) {
    console.log('обработка заявления....');
    let promise = new Promise(function(resolve, reject){
        setTimeout(function(){

            Math.random() > 0
                ? resolve({ status: "success"})
                : reject("В визе отказано: не те документы");

        }, 1000); // симулируем задержку документов
    });
    return promise;
}

function getVisa(visa){
    // then автоматически создает новое обещание и передает его дальше по цепочке
    /********** I ВАРИАНТ *********/
    /*
    console.dir(visa);
    console.info("Виза получена ХОП");
    // используем return, чтобы передать visa как параметр next Promise (в функцию bookHotel)
    return visa;
    */
    /********** II ВАРИАНТ (вернем обещание непосредственно) *********/
    console.log("visa from getVisa", visa);
    console.info("Виза получена ХОП");
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            resolve(visa)
        }, 2000)
    });
}

function bookHotel(visa) {
// then автоматически создает новое обещание и передает его дальше по цепочке
/********** I ВАРИАНТ *********/
/*
    console.log("visa: ", visa);
    console.log("Бронируем отель");
    return {status: "success Hotel"};
*/
    console.log("visa from bookHotel: ", visa);
    console.log("Бронируем отель");
    return new Promise(function (resolve, reject) {
        //reject("НЕТ МЕСТ")
        resolve({status: "success Hotel"});
    });
}

function buyTickets(booking) {
    console.log("Покупаем билеты");
    console.log("Бронь: ", booking);
}
applyForVisa({ data: "docs"})
    // then автоматически создает новое обещание и передает его дальше по цепочке
    .then(
        getVisa
    ).
    then(
        bookHotel
    ).
    then(
        buyTickets
    ).
    catch(
        (error) => {
            console.error(error)
        }
    );

/*************************** CALLBACK HELL: ************************************/
/*
 // чтобы вернуть visa (из setTimeout) определим 2-м параметр ф-ю обратного вызова:
 // если нам не одобряют документы воспользуемся еще одним callback - 3-й параметр

 function applyForVisa(documents, resolve, reject) {
     console.log('обработка заявления....');
     setTimeout(function(){

         Math.random() > .5 ? resolve({ kuku: "kuku"}) : reject("В визе отказано: не те документы");

         let visa = {};

     }, 2000); // симулируем задержку документов
 }

applyForVisa(
    { data: "docs"},
    function(visa){
        console.info("Виза получена");
        // после того как мы получили визу:
        bookHotel(visa, function(reservation){
            buyTickets(reservation, function () {
            }, function () {

            })

        }, function(error){

        } );
    },
    function(reason){
        console.error(reason);
    }
);
*/
// CALLBACK HELL нам помогут решить обещания
/*********Использование обещаний на практическом примере************/

/*
IE не поддерживает обещания.
Решить эту проблему нам поможет http://babeljs.io/docs/usage/polyfill/

*/

// Перепишем следующий пример на PROMISE
/*
let movieList = document.getElementById('movies');

function addMovieToList(movie) {
    let img = document.createElement("img");
    img.src = movie.Poster;
    movieList.appendChild(img);
}

function getData(url, done) {

    let xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = function () {
        if(xhr.status === 200) {
            let json = JSON.parse(xhr.response);
            console.log(json);
            done(json.Search);
        }
        else {
            console.error(xhr.statusText);
        }
    };

    xhr.onerror = function (error) {
        console.error(error);
    };

    xhr.send();
}

let search = "batman";

getData(`http://www.omdbapi.com/?s=${search}`, function (movies) {
    movies.forEach(function (movie) {
        addMovieToList(movie);
    })
})
*/

let movieList = document.getElementById('movies');

function addMovieToList(movie) {
    let img = document.createElement("img");
    img.src = movie.Poster;
    movieList.appendChild(img);
}

function getData(url) {

    return new Promise(function (resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.onload = function () {
            if(xhr.status === 200) {
                let json = JSON.parse(xhr.response);
                console.log(json);
                resolve(json.Search);
            }
            else {
                reject(xhr.statusText);
            }
        };

        xhr.onerror = function (error) {
            reject(error);
        };
        xhr.send();
    })
}

let search = "batman";
/*
getData(`https://www.omdbapi.com/?s=${search}`)
    .then(movies =>
            movies.forEach(movie =>
                addMovieToList(movie)))
    .catch((error) => {console.error(error)});
*/

/*******LET & RACE на конкретном примере**********/

let lady = getData(`https://www.omdbapi.com/?s=lady`);
let man = getData(`https://www.omdbapi.com/?s=man`);
/*
batman
    .then(movies =>
        movies.forEach(movie =>
            addMovieToList(movie)))
    .catch((error) => {console.error(error)});
superman
    .then(movies =>
        movies.forEach(movie =>
            addMovieToList(movie)))
    .catch((error) => {console.error(error)});
*/
Promise.race([lady, man])
    .then(movies =>
        movies.forEach(movie =>
        addMovieToList(movie)))
    .catch((error) => {console.error(error)});;


/*******LET & RACE**********/
function go(num) {
    return new Promise(function (resolve, reject) {
        let delay = Math.ceil(Math.random() * 3000);
        //console.log("num: ", num);
        //console.log("delay: ", delay);
        setTimeout(() => {

            if (delay > 2000) {
                reject(num);
            }
            else {
                resolve(num+"ku");
            }

        }, delay)
    })
}

let promise1 = go(1);
let promise2 = go(2);
let promise3 = go(3);
/************ALL************/
// допустим нам необходимо дождаться выполнений всех обещаний,
// для этого воспользуемся методом all
// в качестве аргумента принимает массив обещаний
//https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
Promise.all([promise1, promise2, promise3])
    .then(value => console.log("value: ", value))// в value находится массив из значений
    .catch(error => console.error(error)); // выводит значение, в котором произошла ошибка
// которые возвращают обещания

/************RACE************/
// допустим нам не важно, чтобы выполнились все обещания
// нам важно получить результат от самого первого, которое выполнится
// для этого есть метод race
Promise.race([promise1, promise2, promise3])
    .then(value => console.log("value: ", value))
    .catch(error => console.error(error));

fetch

Метод fetch позволяет вам делать запросы, схожие с XMLHttpRequest. Основное отличие заключается в том, что Fetch API использует Promise.

let promise = fetch(url[, options]);

/*
url – URL, на который сделать запрос,
options – необязательный объект с настройками запроса.

    method – метод запроса,

    headers – заголовки запроса (объект),

    body – тело запроса: FormData, Blob, строка и т.п.

    mode – одно из: «same-origin», «no-cors», «cors», указывает,
        в каком режиме кросс-доменности предполагается делать запрос.

    credentials – одно из: «omit», «same-origin», «include», указывает,
        пересылать ли куки и заголовки авторизации вместе с запросом.

    cache – одно из «default», «no-store», «reload», «no-cache», «force-cache»,
        «only-if-cached», указывает, как кешировать запрос.

    redirect – можно поставить «follow» для обычного поведения при коде 30x
        (следовать редиректу) или «error» для интерпретации редиректа как ошибки.
*/

// Использование
fetch(this.get_path(path), this.merge_options(Object.assign({method: method}, options)))
    .then(response => {
        RequestEvents.dispatch(method, [response]);
        resolve(this.prepare_response(response))
    })
    .catch(err => {
        reject(err);
    });
Добавить комментарий