Javascript.RU

Вложенные асинхронные вызовы. Объект 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" в одном из трех случаев:

  1. аргумент callback или errback является instanceof Error
  2. из последнего обработчика выпал exception
  3. последний обработчик вернул значение 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 можно легко вынуть и приспособить к собственной библиотеке, если таковая у вас имеется.

Успешной асинхронной разработки.


Автор: Гость (не зарегистрирован), дата: 29 августа, 2008 - 15:32
#permalink

Что-то я дочитал до конца и понял, что Deffered только при наличии фреймворков есть. Можно было это в начале написать))


Автор: chebur (не зарегистрирован), дата: 18 февраля, 2009 - 22:46
#permalink

> С помощью canceller можно реализовать "чистый" обрыв XMLHTTPRequest, и т.п.
что такое "чистый обрыв XHR"?
не совсем понятно зачем нужен canceler, можете привести пример?


Автор: Илья Кантор, дата: 16 января, 2011 - 18:32
#permalink

Для отмены любого асинхронного события.


Автор: Гость (не зарегистрирован), дата: 30 марта, 2009 - 12:44
#permalink

Часто необходимо для работы с вводом в Text-box'ы - когда человек нажал 1ую букву пошёл первый запрос, через несколько миллисекунд человек нажал вторую букву - пошёл второй запрос. Если, например на сервере идёт select с условием where text like '<введённые символы>%' то результат с 2ми буквами может вернуть гораздо меньше строк (в разы) да и выполнится быстрее, скорее всего - в результате получается, что ответ от запроса 2 может придти раньше 1-го, а первый, в свою очередь, может "затереть" второй. Для пользователя это будет выглядеть, так, как будто 2ой запрос не выполнился.


Автор: Гость (не зарегистрирован), дата: 13 июня, 2009 - 20:42
#permalink

если я правильно понимаю, то мы тут сначала вызываем функцию, а потом добавляем колбэки? Но при асинхронном вызове может получиться, что результат будет получен раньше, чем будет добавлен колбэк.


Автор: Гость (не зарегистрирован), дата: 6 июля, 2009 - 17:39
#permalink

В таком случае callback будет вызван сразу же после его добавления к объекту Deferred.


Автор: oleg2, дата: 17 сентября, 2010 - 10:20
#permalink

Если у Deferred вызвать errback с "не ошибкой", например .errback(10) - по описанию получается что Deferred будет в состоянии "success" и вызовы начнутся с callback- в не errback-функций.

Так ли это? Правильно ли это?


Автор: Ilya Kharlamov, дата: 26 марта, 2011 - 01:16
#permalink

С простыми Deferred всё просто и понятно. Но проблемы начинаются когда начинается вложенность и циклы.
К примеру, есть массив id юзеров

var userIds = [3, 18, 10];

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

var groupNames = ['Admin', 'Guest', 'User']

предположим, есть в наличии два XMLHttpRequest : getGroupIdByUserId и getGroupNameByGroupId
Просто, не правда ли? Но попробуйте написать код, который бы это делал асинхронно, используя Deferred и остался читабельным.


Автор: ashenshakily (не зарегистрирован), дата: 1 мая, 2019 - 16:41
#permalink

Объект Deferred инкапсулирует последовательность обработчиков для еще не существующего результата, чем сильно упрощает сложные AJAX-приложения. Он предоставляется различными фреймворками (Dojo Toolkit, Mochikit) и отдельными библиотечками (madalin stunt cars 2, Promises etc).


 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние темы на форуме
Forum