ECMAScript 6 Promises

На Хабре уже встречались статьи о замечательной технологии Promises, которая в будущем станет частью стандарта ECMAScript 6, однако, в этих статьях не было подробного описания, почему же они так полезны и в чем таки их преимущества. Дабы заполнить этот пробел, я решил написать эту статью.Внимание! Данная статья — не исчерпывающее руководство. Это скорее пиар хорошей и полезной технологии, замануха, показывающая позитивные стороны. Статья является компиляцией нескольких чужих статей, ссылки внизу.

Promise это объект, используемый как заглушка для результата некоего отложенного (и возможно асинхронного) вычисления Способ писать последовательно/параллельно выполняемый асинхронный код как синхронный Часть стандарта ECMAScript 6 Конструктор Promise — это имплементация паттерна Revealing Constructor Pattern «Статичные» методы Promise.resolve (val) and Promise.reject (val) Promise.all and Promise.race Концепция «Thenable» object В двух словах — это объект, обладающий методами .then () and .catch () Fulfilled — вычисление было успешным Rejected — ошибка при вычислении (любая) Pending — вычисление еще не завершилось (не fulfilled и не rejected) Settled — вычисление завершилось (не важно как) Итак, возьмем кусок синхронного кода function foo () { var a = «a»; a = a + «b»; a = a + «c»; return a; } И сделаем так, чтобы снаружи он выглядел как Promise: function foo () { var a = «a»; a = a + «b»; a = a + «c»; return Promise.resolve (a); } Следующий шаг — раздели каждый этап вычисления: function foo () { return Promise.resolve («a») .then (function (a){ return a + «b»; }) .then (function (a){ return a + «c»; }); } Теперь каждый шаг можно сделать асинхронным, причем все выполнение будет по-прежнему последовательным.Пойдем дальше, заменим один из шагов на «как бы асинхронную функцию, возвращающую Promise»: function getB (a){ return Promise.resolve (a + «b»); }

function foo () { return Promise.resolve («a») .then (function (a){ return getB (a); }) .then (function (a){ return a + «c»; }); } Встроенная функциональность допускает возврат в then (cb ()) либо ошибки (throw new Error ()) либо значения (return a+'c';) либо следующего Promise.Параллелизм Представим, что сперва надо выполнить асинхронное действие 1, затем параллельно 2 и 3, и затем 4. asyncAction1() .then (function (res1){ return Promise.all ([async2(res1), async3(res1)]); }). .then (function (arr){ // an array of values var res2 = arr[0], res3 = arr[1]; return asyncAction4(res2, res3); }). then (…); Обработка ошибок Самая замечательная вещь в Promises — это обработка ошибок. Не важно, на каком этапе и в какой глубине вложенности произошла ошибка, будь то reject или просто брошенное исключение, все это можно поймать и обработать, либо же прокинуть дальше. asyncAction () .catch (function (rejection){ // пытаемся понять, можно ли как-то обработать ошибку if (rejection.code == «foo») return «foo»; // никак нельзя, прокидываем ошибку дальше throw rejection; }) .then (…) .then (…) .catch (…); Здесь нужно сделать замечание, что если, положим var p1 = new Promise (…), p2 = new Promise (…) p3 = Promise.all ([p1, p2]);

p3.then (…).catch (…); Catch будет ловить все, что пошло не так (причем не важно, что и как именно) в p1, p2, p3 и любых вложенных вызовах, что дико удобно. Обратная сторона — если catch () нет, то ошибка будет тихо проглочена. Однако библиотеки типа Q, как правило, имеют возможность задать обработчик непойманных ошибок, где их можно вывести в консоль или сделать что-то еще.Слово об анти-паттернах function anAsyncCall () { var promise = doSomethingAsync (); promise.then (function (){ somethingComplicated (); }); return promise; } Иии легким движением руки мы потеряли второй Promise. Дело в том, что каждый вызов .then () или .catch () создает новый Promise, поэтому если создали новый, а вернули старый, то новый повиснет где-то в воздухе и никто не узнает, каков результат вычисления. Как бороться — просто вернуть новый Promise: return promise.then (…);

Задержка выполнения function delay (ms){ return new Promise (function (resolve){ setTimeout (resolve, ms); } }; Пример использования delay (5000).then (…); Простейший таймаут Поскольку Promise может быть settled только один раз (остальное игнорируется), то можно написать что-то вроде function timeout (promise, ms) { return new Promise (function (resolve, reject) { promise.then (resolve); setTimeout (function () { reject (new Error ('Timeout»)); }, ms); }); }

timeout (asyncAction (), 5000).then (…).catch (…); Кто первый встал — того и тапки.Немного улучшенный таймаут Чуть более очевидный пример таймаута через «статичную» функцию Promise.race (). Promise.race ([ asynchronousAction (), delay (5000).then (function () { throw new Error ('Timed out'); }) ]) .then (function (text) { … }) .catch (function (reason) { … });

Библиотека контролирует процесс выполнения, соответственно она заведует, как доставляется результат — синхронно или асинхронно В то же время, спецификация Promises/A+ требует, чтобы всегда использовался последний режим — асинхронный Таким образом, мы всегда можем полагаться на немедленное выполнение кода promise.then ().catch () и т.д., и не заботиться, что какой-то их коллбеков съест все процессорное время (с оговорками, само собой) Они часть стандарта — умные люди думали и разрабатывали всякое для нашего удобства «Практически» не сказываются на производительности (http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of) Попробуйте разрулить нетривиальный поток выполнения на callbacks, желательно с обработкой ошибок, если вы не напишете свою имплементацию Promises, код, скорее всего, будет невозможно читать и понимать Все Promises/A+ совместимые библиотеки могут принимать объекты друг друга (Angular прекрасно работает с объектами Q/Native Promise/RSVP и т.д.) А еще Promise — лучше, чем Deferred, потому что последний — это две концепции в одной, что безусловно плохо, а кроме этого, паттерн Revealing Constructor почти что гарантирует, что Promise будел settled только в рамках этого конструктора (ну или вы сам себе злобный Буратино) Не подходит для повторяющихся событий (правда, Promises не для того и писались) Не подходит для streams (аналогично) Текущая реализация в браузерах не позволяет следит за progress (собираются тоже включить в стандарт) Реализация «Thennable» в jQuery немного отличается от стандартной — в стандарте аргумент в callback ровно один, в jQuery кол-во аргументов больше, что не мешает использовать объект, полученный из jQuery в нативной реализации. Как правило, больше, чем первый аргумент, ничего от jQuery и не нужно. Плюс существует конструкция: var jsPromise = Promise.resolve ($.ajax ('/whatever.json'));

© Habrahabr.ru