Компания
11,20
рейтинг
20 июля 2014 в 01:58

Разработка → JQuery Deferred — примеры использования

В этой статье я не буду говорить о том зачем в javascript нужны промисы и в частности JQuery.Deferred. Также не буду приводить справочную информацию, ее достаточно в интернете. Например тут или тут или вот тут.
Эта статья для тек кто уже немного знаком с объектом Deferred из библиотеки JQuery, но не имеет опыта написания сложных цепочек (очередей).

Подготовка

Все примеры кода, рассмотренные здесь, используют асинхронный метод $.ajax(), который возвращает так называемый jqXHR в котором реализованы промис-методы (done, fail, always, then). Нам нужны будут только они, поэтому будем считать что $.ajax возвращает промис (promise).
В некоторых примерах используются методы $.map() и $.each(), которые входят в состав библиотеки JQuery.

Последовательное выполнение

Простейшее использование промисов — это последовательное выполнение асинхронных операций. То есть следующая операция не начинается пока текущая не закончится.

$.ajax('http://echo.jsontest.com/id/1')
.then(function(result){
    console.log(JSON.stringify(result));
    return $.ajax('http://echo.jsontest.com/id/2')
}).then(function(result){
    console.log(JSON.stringify(result));
    return $.ajax('http://echo.jsontest.com/id/3')
}).then(function(result){
    console.log(JSON.stringify(result));
});

Живой пример тут.

Параллельное выполнение

Запускает все асинхронные операции одновременно и переходит к выполнению следующего колбэка только тогда когда будут выполнены все параллельные операции.

$.when(
    $.ajax('http://echo.jsontest.com/id/1'),
    $.ajax('http://echo.jsontest.com/id/2'),
    $.ajax('http://echo.jsontest.com/id/3')
).then(function(result1, result2, result3){
    console.log(JSON.stringify(result1[0]));
    console.log(JSON.stringify(result2[0]));
    console.log(JSON.stringify(result3[0]));
})

Живой пример тут.

Последовательно-параллельное выполнение

Задача: выполнить один запрос, а после его завершения запустить параллельное выполнение еще нескольких операций.

$.ajax('http://echo.jsontest.com/id/1')
.then(function(result1){
    console.log(JSON.stringify(result1));
    return $.when(
        $.ajax('http://echo.jsontest.com/id/2'),
        $.ajax('http://echo.jsontest.com/id/3'),
        $.ajax('http://echo.jsontest.com/id/4')
    )
}).then(function(result2, result3, result4){
    console.log(JSON.stringify(result2[0]));
    console.log(JSON.stringify(result3[0]));
    console.log(JSON.stringify(result4[0]));
})

Живой пример.

Параллельное выполнение неизвестного количества асинхронных операций

Переходим к более сложным ситуациям. Допустим имеется массив со ссылками по каждой из которых необходимо сделать параллельный запрос. Задача усложняется тем, что количество ссылок в массиве заранее не известно.

array = ['/url1', '/url2', ….. '/urlN']

Мы уже знаем, что для параллельного запуска используется метод $.when следующим образом:

$.when(promise1, promise2, … promiseN)

Но, к сожалению, нельзя передать в этот метод массив промисов. Поэтому придется этот код можно немного переписать, и тогда он подойдет для нашей задачи:

$.when.apply(this, [promise1, promise2, … promiseN])

А вот полностью решение задачи:

urls = ['http://echo.jsontest.com/id/1', 'http://echo.jsontest.com/id/2', 'http://echo.jsontest.com/id/3']
promises = $.map(urls, function(url){
    return $.ajax(url).then(function(result){
        console.log(JSON.stringify(result));
    });
});
$.when.apply(this, promises)
.then(function(){
    console.log('done');
});

Этот же код на jsfiddle.

Последовательное выполнение неизвестного количества асинхронных операций

Задача как в предыдущем примере, но запросы надо отправлять последовательно. Такой подход поможет в случае если запросов очень много, а серверная часть веб-приложения не рассчитана на такие нагрузки.
Для решения этой задачи будем “наращивать” цепочку промисов в цикле.

urls = ['http://echo.jsontest.com/id/1', 'http://echo.jsontest.com/id/2', 'http://echo.jsontest.com/id/3']
promise = $.when();
$.each(urls, function(index, url){
    promise = promise.then(function(){
        return $.ajax(url);
    }).then(function(result){
        console.log(JSON.stringify(result));
    });
});
promise.then(function(){
    console.log('OK');
});

Здесь я применил небольшой трюк: promise = $.when(). Запуск метода $.when без аргументов вернет resolved промис, который станет первым звеном цепочки.
Посмотреть код в действии.

Простая обработка ошибок: один обработчик для всех операций

Для обработки ошибок используется метод .fail. В примере ниже этот метод находится в самом конце цепочки промисов и при возникновении ошибки все done-колбэки пропускаются.

$.ajax('http://echo.jsontest.com/id/1')
.then(function(){
    console.log('OK 1');
    return $.ajax('http://echo.jsontest.com/id/2');
}).then(function(){
    console.log('OK 2');
    return $.ajax('http://echo.jsontest_fail.com/id/3');
}).then(function(){
    console.log('OK 3');
    return $.ajax('http://echo.jsontest.com/id/4');
}).then(function(){
    console.log('OK 4');
}).fail(function(){
    console.log('error');
});

Запустить этот код.

Остановка выполнения цепочки после обработки ошибки

Если в цепочке используется несколько обработчиков ошибок (rejected промисов), то при возникновении ошибки будут вызваны все последующие fail-колбэки. Пример:

$.ajax('http://echo.jsontest.com/id/1')
.then(function(){
    console.log('OK 1');
    return $.ajax('http://echo.jsontest.com/id/2');
}).then(function(){
    console.log('OK 2');
    return $.ajax('http://echo.jsontest_fail.com/id/3');
}).fail(function(){
    console.log('error 1');
}).then(function(){
    console.log('OK 3');
    return $.ajax('http://echo.jsontest.com/id/4');
}).fail(function(){
    console.log('error 2');
}).then(function(){
    console.log('OK 4');
}).fail(function(){
    console.log('error 3');
});


После выполнения этого кода увидим в консоли следующее:

OK 1
OK 2
error 1
error 2
error 3

Ссылка для тех кто не верит.
Но скорее всего такое поведение нам не пригодится. Сделаем так чтобы после обработки ошибки ни один последующий колбэк не вызвался.

$.ajax('http://echo.jsontest_fail.com/id/1')
.then(function(){
    console.log('OK 1');
    return $.ajax('http://echo.jsontest.com/id/2');
}, function(){
    console.log('error 1');
    return $.Deferred();
}).then(function(){
    console.log('OK 2');
}, function(){
    console.log('error 2');
})

Посмотреть на результат.
В этом примере следует обратить внимание на две вещи. Первое — обработчики ошибок теперь задаются вторым аргументом метода .then. Второе — обработчик ошибок возвращает Deferred объект (промис) который не является ни resolved ни rejected.

Продолжение выполнения цепочки после обработки ошибки

Немного изменив предыдущий пример можно сделать так что после обработки ошибки будет вызван следующий done-колбэк.

$.ajax('http://echo.jsontest_fail.com/id/1')
.then(function(){
    console.log('OK 1');
    return $.ajax('http://echo.jsontest.com/id/2');
}, function(){
    console.log('error 1');
    return $.when();
}).then(function(){
    console.log('OK 2');
}, function(){
    console.log('error 2');
})

Этот пример отличается от предыдущего только седьмой строкой.

Заключение

Компонент JQuery.Deferred не такой сложный каким кажется при первом знакомстве с ним, впрочем такими же простыми являются остальные библиотеки реализующие функционал промисов.

UPD

Спасибо пользователю mayorovp за комментарий
Автор: @format1981
Bankrot-pro
рейтинг 11,20
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Комментарии (15)

  • +2
    Спасибо! А то вложенностью колбэков действительно можно быстро запутаться
  • +10
    Не нравится мне вот этот код:
    $.when.apply(this, promises)
    В качестве this в when передается какой-то случайный объект… Лучше уж писать $.when.apply(null, promises) или $.when.apply($, promises).
  • –1
    Параллельное выполнение

    Запускает все асинхронные операции одновременно и переходит к выполнению следующего колбэка только тогда когда будут выполнены все параллельные операции.


    К сожалению не совсем так. Из-за включенного по умолчанию в современных браузерах http pipelining, запросы будут выполняться последовательно. Т.е. браузер не отправит новый запрос, пока не выполнится предыдущий.
    На своем проекте недавно с этим столкнулся. Там отправляются одновременно несколько ajax запросов на сервер, и, как оказалось во многих броузерах, если один из запросов «задумается», то следующие не начинают выполняться.
    • 0
      Pipelining же как раз чтобы избежать такого, нет? «HTTP requests are sent on a single TCP connection without waiting for the corresponding responses». Единственное что «methods like POST should not be pipelined. Sequences of GET and HEAD requests can always be pipelined»
    • 0
      Разве в chrome не отключено по умолчанию? В firefox отключено, судя по монитору запросов в chrome тоже.
    • +1
      Запросы будут выполняться последовательно в том случае, если они обращаются к одному и тому же url:
      Последовательно
      Результат


      Параллельно
      Результат

  • +3
    Примеры хорошие, но на мой взгляд есть небольшая подмена понятий в Deferred Object и Promise Object (который я бы переводил как промис). Последний получается из первого путём вызова метода promise на объекте jQuery.Deferred:
    function example() {
      var dfd = new jQuery.Deferred();
     
      // Resolve
      // Reject
      // Notify
     
      // Return the Promise so caller can't change the Deferred
      return dfd.promise();
    }
    

    Сам Promise Object согласно справке:
    This object provides a subset of the methods of the Deferred object (then, done, fail, always, pipe, and state) to prevent users from changing the state of the Deferred.

    Т.е., в моём понимании, Promise Object это результат работы, а Deferred Object — способ повлиять на неё.
    • 0
      Напутал немного с понятиями. Спасибо.
  • +1
    А можно как-нибудь понагляднее про цепочку колбэков и еррбэков

    Что-нибудь типа такого: twistedmatrix.com/documents/current/_images/deferred-process.png
    • 0
      Я думаю тут и без схем все понятно. Есть done-колбэки, есть fail-колбэки. В цепочке последовательно выполняются done-колбэки, до тех пор пока не произойдет ошибка. Тогда начинают выполняться все последующие fail-колбэки. Как поменять такое поведение я описал в двух последних примерах. Еще есть always-колбэки, которые выполняются в любом случае. И еще есть progressCallback — это отдельная история.
      Вот небольшая шпаргалка.
      done(doneCallback, doneCallback, ...)
      fail(failCallbacks, failCallback, ...)
      then(doneCallback, failCallback)
      always(alwaysCallback, alwaysCallback, ...)
      
      • 0
        Ничего ваша шпаргалка не проясняет.
        Придётся лезть в сорцы.
    • 0
      Собственно, по вашей ссылке все уже объяснено.

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

      Метод then возвращает новый промиз, и заданные через него обработчики могут вернуть другие промизы, что позвляет управлять потоком обработки, как это было продемонстрировано в пунктах «Остановка выполнения цепочки после обработки ошибки» и в пункте «Продолжение выполнения цепочки после обработки ошибки».
      • 0
        По моей ссылке нарисована реализация deferred в python twisted.

        То, что обработчик, указанный в then резольвит какой-то новый промиз, и его можно разрезольвить созданным промизом — это вот было не очевидно.
        И это появиолсь только в jquery 1.8 о чём в статье ни слова.

        В документации обработчкики для .done, .fail, .always называются Callback, а для .then — Filters.
        В статье они все перемешаны в кучу, и это не добавляет понимания.

        И тутже встаёт вопрос — анахуа собственно перемешывать .fail и .then а потом пытаться прерывать цепочку?
        • 0
          И тутже встаёт вопрос — анахуа собственно перемешывать .fail и .then а потом пытаться прерывать цепочку?

          Потомучто при использовании fail цепочка не останавливается. Такая вот особенность у библиотеки. jsfiddle.net/LDFTF/1/
        • +1
          То, что обработчик, указанный в then резольвит какой-то новый промиз, и его можно разрезольвить созданным промизом — это вот было не очевидно.
          Но об этом на каждом углу пишут, а статья автора все-таки про примеры использования, а не очередной пересказ документации.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка