Javascript.RU

Объект Deferred.

Каждый, кто когда-либо использовал AJAX, знаком с асинхронным программированием. Это когда мы запускаем некий процесс (скажем, XMLHTTPRequest) и задаем функцию callback обработки результата.

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

Один способ - добавлять каллбэки в параметры всех функций. Другой - использовать для управления асинхронностью отдельный объект. Назовем его Deferred.

Такой объект есть, например, в библиотеке Mochikit и во фреймворке dojo.

Начнем - издалека, с простого примера, который прояснит суть происходящего. Если кому-то простые примеры не нужны - следующий раздел: Асинхронное программирование не для чайников. <code>Deferred</code>..

Нужно отправить результат голосования на сервер и показать ответное сообщение.

Посылкой данных на сервер занимается функция sendData. Эта функция - общего вида, вызывается в куче мест.

В зависимости от результата вызывается callback, если все ок, либо errback - в случае ошибки при запросе.

function sendData(url, data, callback, errback) {
	var xhr = new XmlHttpRequest()
	xhr.onreadystatechange = function() {
		if (xhr.readyState==4) {
			if (xhr.status==200) {
				callback(xhr.responseText)  
			} else {
				errback(xhr.statusText)
			}
		}			
	}
	xhr.open("POST", url, true); 
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
	xhr.send("data="+encodeURIComponent(data))
}

Функция processVoteResult обрабатывает JSON-результат ответа сервера.

function processResult(data) {
	var data = eval( '('+data+')' )
	showMessage(data.text)  
}

Обработку ошибок пусть делает функция handleError, для простоты:

function handleError(error) {  alert(error)  }

Используя функцию отправки sendData, обработки результата processResult, теперь можно записать, наконец, метод голосования:

function vote(id) {
	sendData("http://site.ru/vote.php", id, processResult, handleError)
}

В простейшем случае, все - работа закончена. Но полученная реализация обладает рядом недостатков.

  1. Не учитывается возможность ошибки в processResult, например, при вызове eval.
  2. При использовании этого кода нельзя добавить в цепочку вызовов новую функцию updateVoteInfo(), чтобы, она, скажем, сработала после showMessage.

Один способ решения проблемы 2 - это добавить к processResult аргумент callback и вставить вызов updateVoteInfo через промежуточный обработчик результата в функции vote:

function sendData(url, data, callback) {
	var xhr = new XmlHttpRequest()
	xhr.onreadystatechange = function() {
	    if (xhr.readyState==4) { callback(xhr.responseText)  }
	}
	xhr.open("POST", url, true); 
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
	xhr.send("data="+encodeURIComponent(data))
}


function processResult(data, callback) {
	var data = eval( '('+data+')' )
       	showMessage(data.text)         
	callback(data)
}


function vote(id) {	
	var process = function(result) {
		processResult(result, updateVoteInfo)
	}
		
	sendData("http://site.ru/vote.php", id, process, handleError)
}

Надеюсь, в коде все понятно, т.к пока что он довольно простой... Но как сделать так, чтобы исключение внутри любого асинхронного обработчика (например, processResult) не приводило к ошибке javascript и падению всей цепочки?

Чтобы обработка ошибок и исключения была схожей - добавим к обработчику аргумент errback и будем вызывать его при исключении. А заодно сделаем то же самое и с функцией vote.

function sendData(url, data, callback, errback) {
	var xhr = new XmlHttpRequest()
	xhr.onreadystatechange = function() {
		if (xhr.readyState==4) {
			if (xhr.status==200) {
				callback(xhr.responseText)  
			} else {
				errback(xhr.statusText)
			}
		}			
	}
	xhr.open("POST", url, true); 
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
	xhr.send("data="+encodeURIComponent(data))
}


function processResult(data, callback, errback) {
	try {
		var data = eval( '('+data+')' )
		showMessage(data.text)         
		callback(data)
	} catch(e) {
		errback(e.message)
	}                             
}


function vote(id, callback, errback) {
	var process = function(result) {
		processResult(result, callback, errback)
		updateVoteInfo(result)
	}
		
	sendData("http://site.ru/vote.php", id, process, errback)
}

Код получился чуть переусложненный. Сравним с тем же, но синхронным кодом:

function vote(id) {
	try {
		var result = sendData("http://site.ru/vote.php", id)
		processResult(result)
		updateVoteInfo(result)
	} catch(e) {
		handleError(e)
	}
}


function processResult(data) {
	var data = eval( '('+data+')' )
	showMessage(data.text)
}


function sendData(url, data, callback) {
	var xhr = new XmlHttpRequest()
	xhr.open("POST", url, false); 
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
	xhr.send("data="+encodeURIComponent(data))
	if (xhr.status==200) {
		return xhr.responseText	
	} else {
		throw xhr.statusText
	}
}
  • В асинхронном коде аргументы callback/errback
    • их нет при обычном, последовательном программировании
  • В асинхронном коде обратная последовательность действий
    • сначала делается обработчик результата, а потом - вызывается метод
  • Асинхронный код длиннее

Аналогичный пример с объектом Deferred выглядел бы так

function vote(id) {
	var deferred = sendData("http://site.ru/vote.php", id)
	deferred.addCallback(processResult)
	deferred.addCallback(updateVoteInfo)

	deferred.addErrback(handleError)
}



function sendData(url, data) {	
	var xhr = new XmlHttpRequest()
	xhr.open("POST", url, true); 
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')

	var deferred = new Deferred()

	xhr.onreadystatechange = function() {
		if (xhr.readyState==4) {
			if (xhr.status==200) {
				deferred.callback(xhr.responseText)  
			} else {
				deferred.errback(xhr.statusText)
			}
		}			
	}

	xhr.send("data="+encodeURIComponent(data))

	return deferred
}


function processResult(data) {
	var data = eval( '('+data+')' )
	showMessage(data.text)         
	return data
}

Смысл объекта Deferred - состоит в исключении всей асинхронности из кода. Асинхронностью занимается объект Deferred.
А сам код становится проще, понятнее и позволяет легко организовывать цепочки асинхронных вызовов.

Следующая статья описывает сам объект Deferred в деталях.


Автор: Гость (не зарегистрирован), дата: 24 ноября, 2008 - 18:21
#permalink

Имеет смысл написать, что в реализации JavaScript нет встроенного конструктора Deferred, и для использования этого стека обработчиков нужно подключить фреймворк. Иначе это с ходу вводит в заблуждение.

А можно увидеть внутреннюю реализацию Deferred? Уверен, что она не такая сложная, чтобы создавать отдельный класс для решения такой задачи.


Автор: slogic (не зарегистрирован), дата: 4 сентября, 2009 - 17:10
#permalink

Вы сами себе противоречите: навешиваете обработчик на onreadystatechange ПОСЛЕ действия (open, send).


Автор: Michael83, дата: 22 марта, 2010 - 01:12
#permalink

интересный подход, но я немного не понял следующее:

var deferred = sendData("http://site.ru/vote.php", id)
deferred.addCallback(processResult)
deferred.addCallback(updateVoteInfo)

deferred.addErrback(handleError)

сперва делается sendData, а только потом addCallback/addErrback, понятно дело что при честном http-запросе, код выполнится раньше чем придет результат, но если говорить вообще об асинхронном программировании, результат может прийти раньше чем добавятся обработчики. Или может я что-то не так понял?


Автор: Ярик (не зарегистрирован), дата: 31 июля, 2012 - 15:56
#permalink

Ну, теоретически результат может прилететь раньше, но на практике вряд-ли. К тому же, идеологически, в навешивании callback-а не должно быть никакой сложной логики, и тот код должен выполнятся сравнительно моментально.


Автор: Ярик (не зарегистрирован), дата: 31 июля, 2012 - 16:04
#permalink

Дополнение - в самом Deffered должно быть предусмотрено, что если методом addCallback(callback) обработчик добавляется после того, как событие произошло, то callback вызывается сразу-же.


Автор: R.S. (не зарегистрирован), дата: 25 октября, 2015 - 21:44
#permalink

Не просто "должно быть предусмотрено" -- оно там в самом деле предусмотрено.
если ответ придёт раньше, чем будет навешен обработчик - то обработчик запустится немедленно.
Таким образом deferred избавляет нас от кошмарной вложенности callback'ов, код выглядит "последовательным" и более читабельным


Автор: MiF, дата: 19 октября, 2012 - 11:04
#permalink

Такие допущения приводят к "плавающим" ошибкам, которые отловить практически нельзя.
Что касается "в теории": если файл закеширован, а при навешивании все-таки происходят некоторые действия, ответ можно получить раньше чем ожидается.
Что касается "на практике": при отладке поставь точку останова на

deferred.addCallback(processResult)

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


Автор: cyber, дата: 27 июля, 2013 - 10:38
#permalink

нет не должен, вы не учитивает работу событийного цикла.


Автор: Гость (не зарегистрирован), дата: 29 ноября, 2012 - 17:08
#permalink

Почему бы не написать проще, без всяких deferred:

var xhr = new XmlHttpRequest()

function sendData(url, data) {
    xhr.onreadystatechange = send_Handler;
    xhr.open("POST", url, true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
    xhr.send("data="+encodeURIComponent(data))
}

function send_Handler() {
	if (xhr.readyState==4) {
        if (xhr.status==200) processResult(xhr.responseText);
        else handleError(xhr.statusText);
    } 
}

function processResult(data) {
    var data = eval( '('+data+')' )
    showMessage(data.text) 
}

function handleError(error) {  alert(error)  }

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

Учебник javascript

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

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

Интерфейсы

Все об AJAX

Оптимизация

Разное

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

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