Вложенные асинхронные вызовы. Объект Deferred в деталях.
Объект Deferred инкапсулирует последовательность обработчиков для еще не существующего результата, чем сильно упрощает сложные AJAX-приложения. Он предоставляется различными фреймворками (Dojo Toolkit, Mochikit) и отдельными библиотечками (jsDeferred, Promises etc).
С его помощью можно добавлять обработчики не в момент запуска метода (например, посылки запроса XMLHTTPRequest() , а в любой момент после этого.
Основные методы объекта Deferred:
addCallback(функция-обработчик)
addErrback(функция-обработчик)
callback(результат)
errback(результат)
Обычно, когда функция возвращает Deferred , т.е "обещание результата в некоторый момент", программист затем цепляет к нему обработчики результата, которые будут вызваны в той же последовательности, через addCallback/addErrback.
Код, который управляет объектом Deferred , т.е тот код, который дал обещание предоставить результат, должен вызвать callback () или errback () методы в тот момент, когда результат появится. Например, после завершения некоторой асинхронной операции.
При этом будет вызван первый в цепочке обработчик, добавленный при помощи addCallback () или addErrback() соответственно.
Каждый следующий обработчик получит результат, который вернул предыдущий.
Вот - самый простой пример обработчика:
var deferred = new Deferred()
deferred.addCallback(function(result) { return result })
Важный принцип при работе с Deferred : каждый обработчик должен возвращать результат. Так что затем всегда можно продолжить работу с этим результатом.
Например, вот wrapper для XmlHTTPRequest , который возвращает объект Deferred :
function xhrGet(url) {
var deferred = new Deferred()
var xhr = new XmlHttpRequest()
xhr.open("GET", url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState!=4) return
if (xhr.status==200) {
deferred.callback(xhr.responseText)
} else {
deferred.errback(xhr.statusText)
}
}
xhr.send(null)
return deferred
}
А внешний код может добавить к нему обработчики для успешно полученного результата и для ошибки:
var deferred = xhrGet("http://someurl")
deferred.addCallback(function(res) { alert("Result: "+res); return res })
deferred.addErrback(function(err) { alert("Error: "+err); return err })
Внутри Deferred - это просто упорядоченный список пар callback/errback . Методы addCallback , addErrback, addBoth и addCallbacks добавляют в него элементы.
Например, последовательность обработчиков
var deferred = new Deferred()
deferred.addCallback(myCallback)
deferred.addErrback(myErrbac)
deferred.addBoth(myBoth)
deferred.addCallbacks(myCallback, myErrback)
внутри объекта Deferred становится списком:
[
[myCallback, null],
[null, myErrback],
[myBoth, myBoth],
[myCallback, myErrback]
]
Каждый вызов add* добавляет один элемент в список, как показано выше.
Внутри у Deferred есть одно из трех состояний (свойство "fired "):
- -1, еще нет результата
- 0, есть результат "success"
- 1, произошла ошибка "error"
Deferred приходит в состояние "error" в одном из трех случаев:
- аргумент
callback или errback является instanceof Error
- из последнего обработчика выпал
exception
- последний обработчик вернул значение
instanceof Error
Во всех остальных случаях, Deferred находится в состоянии "success ".
Состояние Deferred определяет, какой обработчик будет вызван из следующего элемента последовательности. Если соответствующее значение равно null (например, надо вызвать errback , а элемент имеет вид [callback, null] ), то этот элемент пропускается.
В случае с обработчиками выше, результат будет обработан примерно так (представьте, что все exceptions перехватываются и возвращаются):
// d.callback(result) or d.errback(result)
if(!(result instanceof Error)){
result = myCallback(result);
}
if(result instanceof Error){
result = myErrback(result);
}
result = myBoth(result);
if(result instanceof Error){
result = myErrback(result);
}else{
result = myCallback(result);
}
Полученный результат затем хранится на тот случай, если к последовательности обработчиков будет добавлен новый элемент. Так как результат уже есть, то при добавлении нового обработчика - он тут же будет активирован.
Обработчики, в свою очередь, могут возвращать объекты Deferred .
При этом остальная часть цепочки исходного Deferred ждет, пока новый Deferred не вернет значение, чтобы, в зависимости от этого значения, вызвать callback или errback .
Таким способом можно сделать реализовать последовательность вложенных асинхронных вызовов.
При создании объекта Deferred можно задавать "canceller " - функцию, которая будет вызвана, если до появления результата произойдет вызов Deferred.cancel .
С помощью canceller можно реализовать "чистый" обрыв XMLHTTPRequest , и т.п.
Вызов cancel запустит последовательность обработчиков Deferred с ошибкой CancelledError (если canceller не вернет другой результат), поэтому обработчики errback должны быть готовы к такой ошибке, если, конечно, вызов cancel в принципе возможен.
Объекты Deferred , как правило, используются, чтобы сделать код асинхронным. Обычно, проще всего описать процесс в обычном, синхронном варианте, и затем разделить код, используя Deferred , чтобы отделить обработку асинхронных операций.
Например, вместо того, чтобы регистрировать callback -функцию, которая будет вызвана при окончании операции рендеринга, можно просто вернуть Deferred .
// объявление с callback
function renderLotsOfData(data, callback){
var success = false
try{
for(var x in data){
renderDataitem(data[x]);
}
success = true;
}catch(e){ }
if(callback){
callback(success);
}
}
// использование объявления с callback
renderLotsOfData(someDataObj, function(success){
// handles success or failure
if(!success){
promptUserToRecover();
}
})
Использование Deferred в данном случае не упрощает код, но задает стандартный интерфейс для задания и обслуживания любого количества обработчиков результата асинхронной операции.
Кроме того, Deferred освобождает от беспокойства на тему "а, может, вызов уже произошел?", например, в случае возврата результата из кеша. С Deferred , новые обработчики могут быть добавлены в любой момент, даже если результат уже получен.
function renderLotsOfData(data){
var d = new Deferred();
try{
for(var x in data){
renderDataitem(data[x]);
}
d.callback(true);
}catch(e){
d.errback(new Error("rendering failed"));
}
return d;
}
// использование Deferred
renderLotsOfData(someDataObj).addErrback(function(){
promptUserToRecover();
});
// NOTE: addErrback и addCallback возвращают тот же Deferred,
// так что мы можем тут же добавить в цепочку новые обработчики
// или сохранить deferred для добавления обработчиков позже.
В этом примере renderLotsOfData работает синхронно, так что оба варианта довольно-таки искусственные. Поставим отображение данных в setTimeout (типа идет анимация), чтобы почувствовать, как крут Deferred :
// Deferred и асинхронная функция
function renderLotsOfData(data){
var d = new Deferred()
setTimeout(function(){
try{
for(var x in data){
renderDataitem(data[x]);
}
d.callback(true);
}catch(e){
d.errback(new Error("rendering failed"));
}
}, 100);
return d;
}
// используем Deferred для вызова
renderLotsOfData(someDataObj).addErrback(function(){
promptUserToRecover()
})
// Заметим, что вызывающий код не потребовалось исправлять
// для поддержки асинхронной работы
Благодаря Deferred - почти не пришлось ничего менять, порядок обработчиков соответствует реально происходящему, в общем - все максимально удобно и наглядно!
Объект DeferredList расширяет Deferred .
Он позволяет ставить каллбек сразу на пачку асинхронных вызовов.
Это полезно, например, для асинхронной загрузки всех узлов на одном уровне javascript-дерева, когда хочется сделать что-то после окончания общей загрузки.
Объект Deferred есть в javascript-фреймворках Dojo и Mochikit. Кроме того, и там и там есть вспомогательный объект DeferredList .
Собственно, эта статья написана частично по документации dojo. Автор считает, что имеет на это право, т.к сам приложил руку к этой части разработки данного фреймворка . Впрочем, из обоих фреймворков Deferred можно легко вынуть и приспособить к собственной библиотеке, если таковая у вас имеется.
Успешной асинхронной разработки.
|
Что-то я дочитал до конца и понял, что Deffered только при наличии фреймворков есть. Можно было это в начале написать))
> С помощью canceller можно реализовать "чистый" обрыв XMLHTTPRequest, и т.п.
что такое "чистый обрыв XHR"?
не совсем понятно зачем нужен canceler, можете привести пример?
Для отмены любого асинхронного события.
Часто необходимо для работы с вводом в Text-box'ы - когда человек нажал 1ую букву пошёл первый запрос, через несколько миллисекунд человек нажал вторую букву - пошёл второй запрос. Если, например на сервере идёт select с условием where text like '<введённые символы>%' то результат с 2ми буквами может вернуть гораздо меньше строк (в разы) да и выполнится быстрее, скорее всего - в результате получается, что ответ от запроса 2 может придти раньше 1-го, а первый, в свою очередь, может "затереть" второй. Для пользователя это будет выглядеть, так, как будто 2ой запрос не выполнился.
если я правильно понимаю, то мы тут сначала вызываем функцию, а потом добавляем колбэки? Но при асинхронном вызове может получиться, что результат будет получен раньше, чем будет добавлен колбэк.
В таком случае callback будет вызван сразу же после его добавления к объекту Deferred.
Если у Deferred вызвать errback с "не ошибкой", например .errback(10) - по описанию получается что Deferred будет в состоянии "success" и вызовы начнутся с callback- в не errback-функций.
Так ли это? Правильно ли это?
С простыми Deferred всё просто и понятно. Но проблемы начинаются когда начинается вложенность и циклы.
К примеру, есть массив id юзеров
Нужно к примеру, асинхронно вернуть названия групп в которых состоят эти юзеры. (Пример придуман из головы чтобы показать проблему).
предположим, есть в наличии два XMLHttpRequest : getGroupIdByUserId и getGroupNameByGroupId
Просто, не правда ли? Но попробуйте написать код, который бы это делал асинхронно, используя Deferred и остался читабельным.
Объект Deferred инкапсулирует последовательность обработчиков для еще не существующего результата, чем сильно упрощает сложные AJAX-приложения. Он предоставляется различными фреймворками (Dojo Toolkit, Mochikit) и отдельными библиотечками (madalin stunt cars 2, Promises etc).