Что плохого в коллбэках (callback)
В колбэках, в общем-то, ничего плохого нет. Проблема может возникнуть, однако, когда вы хотите, чтобы один колбэк вызывался строго после выполнения другого. Например, вы отправляете AJAX-запрос, и только когда будет получен ответ, вы хотите отправить еще один запрос с другой колбэк-функцией. Отлично. Но что если у вас три, четыре, десять таких пар запрос-колбэк? Что, если вы хотите отправить три запроса на сервер, а колбэк-функцию вызвать, только когда все три вернут что-то с сервера? Всё это может привести к печально выглядящему коду:
fs.readdir(source, function (err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
Пример взят с сайта, который так и озаглавлен — ад колбэков (callbackhell.com). Чтобы решить озвученные (а также более сложные и менее очевидные) задачи, была придумана концепция обещаний (promises).
Что такое deferred и promise
Концепция обещаний позволяет обращаться с асинхронными запросами, будто они синхронные. Обещания (promises) — это значения, которые еще только должны быть вычислены в будущем. Однако описывать то, что с ними нужно делать, когда они будут вычислены, можно заранее. Обычный JavaScript поддерживает эту концепцию, начиная со спецификации ECMAScript 2015. Подробнее ознакомиться с этой реализацией можно на сайте консорциума Mozilla MDN. Поскольку стандарт пока используется достаточно ограниченно, лучше воспользоваться реализацией этой концепции в одной из библиотек. Я остановлюсь в данной статье на jQuery, как самой популярной.
Лучше всего концепция объяснена у Криса Уэбба , я возьму с его блога несколько примеров. В jQuery введена конструкция Deferred-объекта. Этот объект представляет асинхронное действие, которое еще не выполнено (с отложенным выполнением). Каждый deferred-объект имеет внутри себя promise-объект, который представляет значение, которое пока неизвестно. У обещаний, в свою очередь, есть состояния (resolved, failed) и обработчики, которые определяют, что делать в ситуации смены состояния.
Канонический пример, использующий Deferred и содержащееся в нем обещание:
var deferred = $.Deferred(); deferred.done(function(value) { alert(value); }); deferred.resolve("hello world");
Здесь мы определили deferred-объект, затем через метод done содержащегося в нем обещания назначаем функцию, выводящую в окне будущее значение, которое нам будет известно, когда deferred будет выполнен (resolved). Наконец, вручную выполняем deferred со значением «hello world». Предсказуемо, эта строка выводится во всплывающем окне.
Основные методы Promise и Deferred
Самый простой реалистичный сценарий использования обещания:
var request = $.ajax(url); request.done(function () { console.log('Request completed'); });
Метод jQuery ajax возвращает обещание. В примере через метод done мы задаем функцию, которая будет выполнена, если запрос пройдет успешно (не вернет ошибку).
deferred-Объект начинает свой путь в подвешенном состоянии (pending). Изменить его состояние можно функциями resolve() и reject(). Тогда он перейдет в статус, соответственно, resolved (разрешенный, успешно отработавший) и rejected (отмененный, отработавший с ошибкой). В конечный статус он может перейти лишь один раз.
Обещание Promise — это неизменяемый (immutable) объект. То есть что-то менять можно только в Deferred, но не в Promise.
Promise предоставляет следующие функции:
state() — получить состояние объекта
done() — позволяет привязать обработчик события смены статуса на resolved
fail() — привязывает обработчик события смены статуса на rejected
then() — объединяет возможности done и fail (может привязать сразу два обработчика) + позволяет выстраивать обработчики в цепочки (об этом далее по тексту)
С помощью конструкции $.when() можно объединять условия вызова обработчиков. Например, если мы отправили на сервер два асинхронных запроса и хотим вызвать обработчик только после того, как оба запроса вернут данные.
Интересная особенность Deferred — если он был выполнен (resolved) до того, как обработчик на это событие был назначен, то этот обработчик выполнится немедленно:
var deferred = $.Deferred(); deferred.resolve("hello world"); deferred.done(function(value) { alert(value); });
Примеры использования
Основные варианты использования крутятся вокруг трех вещей (взято из Promise & Deferred Objects in JavaScript Pt.2: in Practice):
Несколько обработчиков на одно событие
var request = $.ajax(url); request.done(function () { console.log('Запрос выполнен'); }); // В другом месте приложения request.done(function (retrievedData) { $('#contentPlaceholder').html(retrievedData); });
Обработчик на выполнения двух задач сразу
$.when(taskOne, taskTwo).done(function () { console.log('taskOne и taskTwo выполнены'); });
Последовательное выполнение асинхронных задач
Когда одна задача выполнена, начинает выполняться вторая
var step1, step2, url; url = 'http://fiddle.jshell.net'; step1 = $.ajax(url); step2 = step1.then( function (data) { var def = new $.Deferred(); setTimeout(function () { console.log('Запрос выполнен'); def.resolve(); },2000); return def.promise(); }, function (err) { console.log('Step1 завершился неудачей: Ajax запрос'); } ); step2.done(function () { console.log('Последовательность выполнена') setTimeout("console.log('end')",1000); });
Практическое применение этих шаблонов может быть в AJAX-запросах, анимации, синхронизации параллельных задач при помощи $.when(), развязывании событий и программной логики и т.п.
Особенности работы then()
- .then() всегда возвращает новое обещание (Promise)
- .then() ожидает функцию в качестве параметра
Если в .then() не передана функция:
- новое обещание будет повторять поведение исходного обещания (что означает, что оно будет немедленно выполнено/отклонено)
- ввод внутри .then() будет выполнен, но проигнорирован .then()
если в .then() передана функция, которая возвращает Promise-объект:
- новое обещание будет иметь то же поведение, что и возвращаемое обещание
- если в .then() передана функция, которая возвращает значение, то это значение становится значением нового объекта
А зачем нам использовать jQuery?
В данный момент обещания уже входят в стандарт ECMA и реализованы нативно почти во всех современных браузерах. При этом реализована спецификация Promise/A. Многие библиотеки, реализующие сходную концепцию, совместимы с этой спецификацией, однако, это не случай jQuery, в котором всё сделано не так. Если вы активно пользуетесь jQuery, но предпочитаете вместо Deferred-объектов использовать нативные Promise для JavaScript, как это рекомендовано, например, в статье Джейка Арчибальда, то в этой же статье дается совет преобразовывать полученные из методов jQuery обещания к стандартному объекту Promise так скоро, как возможно. Например, так:
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
$.ajax() возвращает Deferred-объект jQuery, но этот объект легко приводится к стандартному Promise указанным способом.
Еще по теме и использованные материалы (англ.)
- Promise & Deferred Objects in JavaScript Pt.2: in Practice
- Understanding JQuery.Deferred and Promise
- An introduction to jQuery Deferred / Promise and the design pattern in general
- Deferred and promise in jQuery
- Provide a default ‘fail’ method for a jQuery deferred object
- The Deferred anti-pattern
- jQuery, Promises & Deferred