13 июня в DOM Standard внесли окончательную версию спецификации Promises. Эта спецификация уже какое-то время присутствовала в тексте стандарта под названием Futures, но находилась на рассмотрении и уточнении. А теперь, стало быть, приняли.
Разработчики браузеров уже вовсю внедряют: Mozilla отрапортовала соответствующий баг как Resolved Fixed, Google в Хроме еще пилит. Про Микрософт не знаю, но думаю в будущей версии IE — сделают. Теперь все и везде сделают — это теперь часть стандарта, да и сама по себе вещь просто прекрасная. После ее появления, NodeJS, например, станет намного юзабельнее 🙂
Подробнее о DOM Promises — ниже.
Знаете что такое асинхронные вызовы в javascript? Это вызовы функций, которые требуют времени на выполнение, но при этом не блокируют основной поток программы. Например, ajax-запрос. Вы его отправляете — а когда он там выполнится никому не известно. Может через 5 мс, может через 200, а может сеть лаганула — и вообще через пару секунд. Чтобы браузер (или NodeJS) не зависал на время выполнения таких запросов (а также запросов чтения из файловой системы, запросов к базе данных, да и вообще любых потенциально длительных операций), все эти запросы выполняются вне основного потока программы.
Но нам же надо получить и обработать результат запроса! Вывести из него на экран данные например. Вот это уже делается множеством разных способов. Самая популярная библиотека JQuery — практикует расстановку коллбеков как аргументов своих функций. Другие библиотеки — кто во что горазд. Создают объекты определенного типа и вызывают события на нем, вызывают события на глобальных объектах, используют временные объекты для хранения функций-коллбеков, или даже действуют как JQuery, но передают коллбек не аргументом, а свойством в объекте со своими настройками.
И это еще не вся беда. Библиотеки могут по-разному обрабатывать ошибки в асинхронных операциях. У них может быть, а может и не быть способа регистрации нескольких коллбеков в зависимости от результата. У них может не быть способа получения результата операции после ее завершения.
Promises решают все эти поблемы. Когда функция запускает асинхронное задание, она должна вернуть Promise. Пользователь может зарегистрировать коллбеки на успешное и неуспешное выполнение этого задания. Можно зарегистрировать не по одному, а по несколько коллбеков и на успех, и на ошибку — и все они будут вызваны в соответствии с результатом асинхронного запроса. Коллбеки можно добавлять даже когда запрос уже выполнен — они просто будут выполняться сразу же (в следующий «тик»), получая результат совершённого запроса.
Классно? На самом деле, все это можно было делать и раньше, если возвращать результатом операции объект EventTarget и просто вызывать события. Но, и это самое главное: Promise — это first-class value, поэтому с ним можно использовать дополнительные абстракции.
1. Цепочки вызовов, chaining.
Функция регистрации коллбека для Promise — тоже возвращает Promise. Поэтому, теперь элементарно можно сделать последовательные вызовы после получения значения асинхронного запроса, и они будут выполняться именно по-очереди:
AsyncFunc().then(callback1).then(callback2);
2. Наследующие цепочки вызовов.
А что если в коллбеке асинхронной функции надо выполнить еще одну асинхронную функцию и передать в нее еще один коллбек? Кто писал на NodeJS — знает что такое NESTING HELL! Так вот, как я сказал выше, при регистрации коллбека в Promise(1) возвращается еще один Promise(2). Но если сам коллбек тоже возвращает Promise(3) — то Promise(2) унаследует его состояние! Это означает, что теперь можно последовательно вызывать асинхронные вызовы, не вкладывая их друг в друга:
AsyncFunc().then( function(val) { return otherAsyncFunc(); }).then( function(val) { // запустится только когда завершится otherAsyncFunc()! return oneMoreAsyncFunc(); }).then( function(val) { //запустится только после завершения oneMoreAsyncFunc()! });
3. Линейный рост количества коллбеков.
Если вам приходилось регистрировать разные коллбеки на успешное и ошибочное выполнение асинхронного вызова, то вы знаете, что количество коллбеков росло экспоненциально:
oldAsyncFunc(function(success) { return anotherAsync(function(success) { ... }, function(error) { ... }); }, function(error) { return yetAnotherAsync(function(success) { ... }, function(error) { ... }); });
Здесь, на два последовательных асинхронных вызова пришлось объявить шесть функций. А если в цепочку добавится еще один вызов — количество функций вырастет до 14. Конечно, на практике большинство использует именованные функции, а не вложенные — иначе в коде получается нечитабельная лапша.
Обычно, коллбек обработки ошибки возвращает значение «по-умолчанию», такого же типа как и при успешном выполнении. Ну, чтобы код дальше работал без проблем. Promises упрощают именно этот паттерн:
newAsyncFunc().then(function(success) { return anotherAsync(); }, function(error) { return default1; }).then(function(success) { return yetAnotherAsync(); }, function(error) { return default2; });
В этом коде всего четыре объявления функций, по два — на каждый уровень вложенности запросов. Если добавится еще один уровень вложенности — функций будет всего шесть. Нет дублирования кода.
4. Простота обработки ошибок.
В обычном асинхронном коде, ошибки — абсолютное зло. Куда вы поставите try/catch при использовании коллбеков? Нельзя обернуть функцию целиком, потому что она сразу и успешно выполняется, нельзя обернуть коллбек, потому что он будет запущен только после завершения асинхронного запроса. Единственный вариант — изобретать систему врапперов.
Ну а в Promises такой проблемы вообще не стоит. Каждое Promise принимает коллбеки для успешного и ошибочного выполнения, так что если хоть один результат в цепочке выкинет ошибку — все последующие тоже автоматически вызовут коллбек обработки ошибки.
5. Комбинаторы
Одна из самых больших сложностей при использовании асинхронных вызовов — это сложность их синхронизации. Что делать, если вам надо выполнить два асинхронных запроса и после завершения обоих, выполнить функцию, обрабатывающую их результат? Вам придется вручную писать примитивную синхронизацию с использованием дополнительных функций, использующих общие объекты для выяснения кто из них выполнился последним и должен запустить обработку результатов.
Promises позволяют проводить три вида синхронизации:
- Promise.every(): Если все переданные в эту функцию Promises выполнятся успешно, то итоговая функция получит массив результатов их выполнения, иначе будет вызван коллбек ошибки.
- Promise.some(): Если хоть одно из переданных сюда Promises выполнится успешно, то итоговая функция получит его результат. Если ни одно Promise не выполнится — будет запущен коллбек ошибки с массивом результатов ошибочных коллбеков.
- Promise.any(): Как только хоть одно из переданных Promises завершится — будет вызван соответствующий успешный или ошибочный коллбек.
Например, мы хотим выполнить два ajax-запроса, а потом обработать их результат вместе:
Promise.every(getJSON(url1), getJSON(url2)).then(function(arr) { // Выполняем общую операцию над arr[0] и arr[1], содержащие результаты getJSON }, function(error) { // Обрабатываем ошибку если хоть один из getJSON не выполнился. });
Попробуйте сделать такое на JQuery — это будет нетривиальное и забавное упражнение. 🙂
По мере распространения реализации Promises в браузерах и движках, они безусловно станут использоваться везде. Это именно тот функционал, который сдерживал распространение NodeJS и многочисленных javascript-библиотек.
В основе записи — перевод статьи Explaining Futures с адаптацией под DOM Standard.