Статьи

Решать асинхронные задачи с обещаниями JQuery

Обещания — это захватывающая функция jQuery, которая позволяет легко управлять асинхронными событиями. Они позволяют вам писать более четкие и короткие обратные вызовы и держать логику приложения высокого уровня отдельно от поведения низкого уровня.

Как только вы поймете Promises, вы захотите использовать их для всего: от вызовов AJAX до потока пользовательского интерфейса. Это обещание!


Как только Обещание разрешено или отклонено, оно останется в этом состоянии навсегда.

Обещание — это объект, представляющий одноразовое событие, обычно результат асинхронной задачи, такой как вызов AJAX. Сначала Обещание находится в состоянии ожидания . В конце концов, он либо решается (что означает, что задача выполнена), либо отклоняется (если задача не выполнена). Как только Обещание разрешено или отклонено, оно останется в этом состоянии навсегда, и его обратные вызовы никогда не сработают снова.

Вы можете прикрепить обратные вызовы к Обещанию, которые будут срабатывать при разрешении или отклонении Обещания. И вы можете добавить больше обратных вызовов, когда захотите — даже после того, как Обещание было разрешено / отклонено! (В этом случае они сразу же выстрелят.)

Кроме того, вы можете логически объединять Обещания в новые Обещания. Это упрощает написание кода, который говорит: «Когда все эти вещи произошли, сделай это».

И это все, что вам нужно знать об обещаниях в резюме. Есть несколько реализаций JavaScript на выбор. Двумя наиболее заметными являются q Криса Ковала , основанные на спецификации CommonJS Promises / A , и jQuery Promises (добавлено в jQuery 1.5). Из-за повсеместного распространения jQuery мы будем использовать его реализацию в этом руководстве.


Каждое обещание JQuery начинается с отложенного. Отложенный — это просто Обещание с методами, которые позволяют его владельцу разрешать или отклонять его. Все остальные Обещания являются «только для чтения» копиями Отложенных; мы поговорим о них в следующем разделе. Чтобы создать Deferred, используйте конструктор $.Deferred() :

Отложенный — это просто Обещание с методами, которые позволяют его владельцу разрешать или отклонять его.

1
2
3
4
5
6
var deferred = new $.Deferred();
 
deferred.state();
deferred.resolve();
deferred.state();
deferred.reject();

( Примечание к версии: state() была добавлена ​​в jQuery 1.7. В 1.5 / 1.6 используйте isRejected() и isResolved() .)

Мы можем получить «чистый» Promise, вызвав метод Deferred promise() . Результат идентичен отложенному, за исключением того, что методы resolve() и reject() отсутствуют.

1
2
3
4
5
6
var deferred = new $.Deferred();
var promise = deferred.promise();
 
promise.state();
deferred.reject();
promise.state();

Метод promise() существует исключительно для инкапсуляции: если вы возвращаете Deferred из функции, он может быть разрешен или отклонен вызывающей стороной. Но если вы только вернете чистое Обещание, соответствующее этому Отложенному, вызывающий может только прочитать его состояние и присоединить обратные вызовы. Сам jQuery использует этот подход, возвращая чистые Promises из своих методов AJAX:

1
2
3
4
var gettingProducts = $.get(«/products»);
 
gettingProducts.state();
gettingProducts.resolve;

Использование времени в названии Обещания проясняет, что оно представляет процесс.


Получив Обещание, вы можете прикрепить столько обратных вызовов, сколько захотите, используя методы done() , fail() и always() :

01
02
03
04
05
06
07
08
09
10
11
promise.done(function() {
  console.log(«This will run if this Promise is resolved.»);
});
 
promise.fail(function() {
  console.log(«This will run if this Promise is rejected.»);
});
 
promise.always(function() {
  console.log(«And this will run either way.»);
});

Замечание по версии: always() упоминалось как complete() до jQuery 1.6.

Существует также сокращение для присоединения всех этих типов обратных вызовов одновременно, then() :

1
promise.then(doneCallback, failCallback, alwaysCallback);

Обратные вызовы гарантированно выполняются в том порядке, в котором они были присоединены.

Одним из отличных вариантов использования Promises является представление ряда потенциальных действий пользователя. Давайте возьмем базовую форму AJAX, например. Мы хотим убедиться, что форма может быть отправлена ​​только один раз, и что пользователь получает подтверждение при отправке формы. Кроме того, мы хотим сохранить код, описывающий поведение приложения, отдельно от кода, который касается разметки страницы. Это значительно облегчит модульное тестирование и минимизирует объем кода, который необходимо изменить, если мы изменим наш макет страницы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// Application logic
var submittingFeedback = new $.Deferred();
 
submittingFeedback.done(function(input) {
  $.post(«/feedback», input);
});
 
// DOM interaction
$(«#feedback»).submit(function() {
  submittingFeedback.resolve($(«textarea», this).val());
 
  return false;
});
submittingFeedback.done(function() {
  $(«#container»).append(«<p>Thank you for your feedback!</p>»);
});

(Мы используем тот факт, что аргументы, передаваемые для resolve() / reject() , дословно передаются каждому обратному вызову.)


pipe() возвращает новое Promise, которое будет имитировать любое Promise, возвращаемое одним из обратных вызовов pipe() .

Наш код формы обратной связи выглядит хорошо, но есть возможности для улучшения взаимодействия. Вместо того, чтобы оптимистично предполагать, что наш вызов POST будет успешным, мы должны сначала указать, что форма была отправлена ​​(скажем, с помощью счетчика AJAX), а затем сообщить пользователю, была ли отправка успешной или неудачной, когда сервер ответит.

Мы можем сделать это, прикрепив обратные вызовы к Обещанию, возвращенному $.post . Но в этом и заключается проблема: нам нужно манипулировать DOM с помощью этих обратных вызовов, и мы поклялись не допустить касающийся DOM код из нашего логического кода приложения. Как мы можем это сделать, когда POST Promise создается в обратном вызове логики приложения?

Решение состоит в том, чтобы «переслать» события разрешения / отклонения из обещания POST обещанию, которое находится во внешней области. Но как мы можем это сделать без нескольких линий мягкого шаблонного promise1.done(promise2.resolve); …)? К счастью, jQuery предоставляет метод для этой цели: pipe() .

pipe() имеет тот же интерфейс, что и обратный вызов then() ( done() обратный вызов reject() обратный вызов always() ; каждый обратный вызов является необязательным), но с одним существенным отличием: while then() просто возвращает Promise, к которому он присоединен (для цепочки) pipe() возвращает новое Promise, которое будет имитировать любое Promise, возвращаемое одним из обратных вызовов pipe() . Короче говоря, pipe() — это окно в будущее, позволяющее нам привязать поведение к Promise, которого еще даже не существует.

Вот наш новый и улучшенный код формы с нашим Обещанием POST, savingFeedback именем savingFeedback :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Application logic
var submittingFeedback = new $.Deferred();
var savingFeedback = submittingFeedback.pipe(function(input) {
  return $.post(«/feedback», input);
});
 
// DOM interaction
$(«#feedback»).submit(function() {
  submittingFeedback.resolve($(«textarea», this).val());
 
  return false;
});
 
submittingFeedback.done(function() {
  $(«#container»).append(«<div class=’spinner’>»);
});
 
savingFeedback.then(function() {
  $(«#container»).append(«<p>Thank you for your feedback!</p>»);
}, function() {
  $(«#container»).append(«<p>There was an error contacting the server.</p>»);
}, function() {
  $(«#container»).remove(«.spinner»);
});

Частью гения Обещаний является их двоичная природа. Поскольку у них есть только два возможных состояния, они могут комбинироваться как логические (хотя логические, значения которых еще не могут быть известны).

Обещательный эквивалент логического пересечения ( AND ) задается $.when() . Учитывая список Обещаний, when() возвращает новое Обещание, которое подчиняется этим правилам:

  1. Когда все данные Обещания разрешены, новое Обещание разрешено.
  2. Когда любое из данных обещаний отклоняется, новое обещание отклоняется.

Каждый раз, когда вы ожидаете появления нескольких неупорядоченных событий, вам следует рассмотреть возможность использования when() .

Одновременные AJAX-вызовы являются очевидным вариантом использования:

1
2
3
4
5
6
7
8
$(«#container»).append(«<div class=’spinner’>»);
$.when($.get(«/encryptedData»), $.get(«/encryptionKey»)).then(function() {
  // both AJAX calls have succeeded
}, function() {
  // one of the AJAX calls has failed
}, function() {
  $(«#container»).remove(«.spinner»);
});

Другой вариант использования позволяет пользователю запрашивать ресурс, который может или не может быть уже доступен. Например, предположим, что у нас есть виджет чата, который мы загружаем с помощью YepNope (см. Раздел Простая загрузка сценариев с помощью yepnope.js ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
var loadingChat = new $.Deferred();
yepnope({
  load: «resources/chat.js»,
  complete: loadingChat.resolve
});
 
var launchingChat = new $.Deferred();
$(«#launchChat»).click(launchingChat.resolve);
launchingChat.done(function() {
  $(«#chatContainer»).append(«<div class=’spinner’>»);
});
 
$.when(loadingChat, launchingChat).done(function() {
  $(«#chatContainer»).remove(«.spinner»);
  // start chat
});

Обещания оказались незаменимым инструментом в продолжающейся борьбе с асинхронным кодом спагетти. Предоставляя двоичное представление отдельных задач, они проясняют логику приложения и сокращают шаблон отслеживания состояния.

Если вы хотите узнать больше об Promises и других инструментах для сохранения вашего здравомыслия в еще более асинхронном мире, ознакомьтесь с моей будущей электронной книгой : Async JavaScript: рецепты для событийно-управляемого кода (выйдет в марте).