Объект 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

Аналогичный пример с объектом 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 в деталях.