Объект 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"):
Deferred приходит в состояние "error" в одном из трех случаев:
callback или errback является instanceof Errorexceptioninstanceof 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 можно легко вынуть и приспособить к собственной библиотеке, если таковая у вас имеется.
Успешной асинхронной разработки.