В этой статье я не буду говорить о том зачем в javascript нужны промисы и в частности JQuery.Deferred. Также не буду приводить справочную информацию, ее достаточно в интернете. Например тут или тут или вот тут.
Эта статья для тек кто уже немного знаком с объектом Deferred из библиотеки JQuery, но не имеет опыта написания сложных цепочек (очередей).
Все примеры кода, рассмотренные здесь, используют асинхронный метод $.ajax(), который возвращает так называемый jqXHR в котором реализованы промис-методы (done, fail, always, then). Нам нужны будут только они, поэтому будем считать что $.ajax возвращает промис (promise).
В некоторых примерах используются методы $.map() и $.each(), которые входят в состав библиотеки JQuery.
Простейшее использование промисов — это последовательное выполнение асинхронных операций. То есть следующая операция не начинается пока текущая не закончится.
Живой пример тут.
Запускает все асинхронные операции одновременно и переходит к выполнению следующего колбэка только тогда когда будут выполнены все параллельные операции.
Живой пример тут.
Задача: выполнить один запрос, а после его завершения запустить параллельное выполнение еще нескольких операций.
Живой пример.
Переходим к более сложным ситуациям. Допустим имеется массив со ссылками по каждой из которых необходимо сделать параллельный запрос. Задача усложняется тем, что количество ссылок в массиве заранее не известно.
Мы уже знаем, что для параллельного запуска используется метод $.when следующим образом:
Но, к сожалению, нельзя передать в этот метод массив промисов. Поэтому придется этот код можно немного переписать, и тогда он подойдет для нашей задачи:
А вот полностью решение задачи:
Этот же код на jsfiddle.
Задача как в предыдущем примере, но запросы надо отправлять последовательно. Такой подход поможет в случае если запросов очень много, а серверная часть веб-приложения не рассчитана на такие нагрузки.
Для решения этой задачи будем “наращивать” цепочку промисов в цикле.
Здесь я применил небольшой трюк: promise = $.when(). Запуск метода $.when без аргументов вернет resolved промис, который станет первым звеном цепочки.
Посмотреть код в действии.
Для обработки ошибок используется метод .fail. В примере ниже этот метод находится в самом конце цепочки промисов и при возникновении ошибки все done-колбэки пропускаются.
Запустить этот код.
Если в цепочке используется несколько обработчиков ошибок (rejected промисов), то при возникновении ошибки будут вызваны все последующие fail-колбэки. Пример:
После выполнения этого кода увидим в консоли следующее:
Ссылка для тех кто не верит.
Но скорее всего такое поведение нам не пригодится. Сделаем так чтобы после обработки ошибки ни один последующий колбэк не вызвался.
Посмотреть на результат.
В этом примере следует обратить внимание на две вещи. Первое — обработчики ошибок теперь задаются вторым аргументом метода .then. Второе — обработчик ошибок возвращает Deferred объект (промис) который не является ни resolved ни rejected.
Немного изменив предыдущий пример можно сделать так что после обработки ошибки будет вызван следующий done-колбэк.
Этот пример отличается от предыдущего только седьмой строкой.
Компонент JQuery.Deferred не такой сложный каким кажется при первом знакомстве с ним, впрочем такими же простыми являются остальные библиотеки реализующие функционал промисов.
Спасибо пользователю mayorovp за комментарий
Эта статья для тек кто уже немного знаком с объектом 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 за комментарий