Deferred и promise в jQuery, или как избавиться от вложенных коллбэков.

Что плохого в коллбэках (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 указанным способом.

Еще по теме и использованные материалы (англ.)

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.