Объект 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)
}
В простейшем случае, все - работа закончена. Но полученная реализация обладает рядом недостатков.
- Не учитывается возможность ошибки в processResult, например, при вызове eval.
- При использовании этого кода нельзя добавить в цепочку вызовов новую функцию
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 в деталях.
|
Имеет смысл написать, что в реализации JavaScript нет встроенного конструктора Deferred, и для использования этого стека обработчиков нужно подключить фреймворк. Иначе это с ходу вводит в заблуждение.
А можно увидеть внутреннюю реализацию Deferred? Уверен, что она не такая сложная, чтобы создавать отдельный класс для решения такой задачи.
Вы сами себе противоречите: навешиваете обработчик на onreadystatechange ПОСЛЕ действия (open, send).
интересный подход, но я немного не понял следующее:
сперва делается sendData, а только потом addCallback/addErrback, понятно дело что при честном http-запросе, код выполнится раньше чем придет результат, но если говорить вообще об асинхронном программировании, результат может прийти раньше чем добавятся обработчики. Или может я что-то не так понял?
Ну, теоретически результат может прилететь раньше, но на практике вряд-ли. К тому же, идеологически, в навешивании callback-а не должно быть никакой сложной логики, и тот код должен выполнятся сравнительно моментально.
Дополнение - в самом Deffered должно быть предусмотрено, что если методом addCallback(callback) обработчик добавляется после того, как событие произошло, то callback вызывается сразу-же.
Не просто "должно быть предусмотрено" -- оно там в самом деле предусмотрено.
если ответ придёт раньше, чем будет навешен обработчик - то обработчик запустится немедленно.
Таким образом deferred избавляет нас от кошмарной вложенности callback'ов, код выглядит "последовательным" и более читабельным
Такие допущения приводят к "плавающим" ошибкам, которые отловить практически нельзя.
Что касается "в теории": если файл закеширован, а при навешивании все-таки происходят некоторые действия, ответ можно получить раньше чем ожидается.
Что касается "на практике": при отладке поставь точку останова на
и все обработчики пойдут лесом. По моему весьма жизненно.
Но подход интересный. В больших проектах асинхронные баги добавляют немало головной боли.
нет не должен, вы не учитивает работу событийного цикла.
Почему бы не написать проще, без всяких deferred: