Но для начала попробуем определить эти самые проблемы, с которыми мы сталкиваемся при работе с асинхронными вызовами.
А для опытов выберем себе пример*, в котором нужно запросить профиль пользователя, новые сообщения и только после этого отобразить страницу. С использованием синхронного выполнения решение было бы таким:
var profile = get_profile(user); var msgs = get_msgs(user); render_page({'profile' : profile, 'msgs' : msgs});
* Все примеры синтетические, в них упущен ряд моментов ради концентрации внимания на самой сути
Проблемы:
1. Большая вложенность кода
Асинхронный вариант нашего примера выглядит так:
var params = {}; get_profile(user, function(profile){ params['profile'] = profile; get_msgs(user, function(msgs){ params['msgs'] = msgs; render_page(params); }) })
При большом количестве вложенный запросов такая запись становится плохо читаемой и трудно отлаживаемой.
2. Параллельные вызовы
В нашем примере все вызовы проходят последовательно, но все они завязаны только на аргумент user и не зависят друг от друга, поэтому хотелось бы для ускорения генерации страницы выполнить их параллельно.
Обычно поступают так:
var params = { 'profile' : null, 'msgs' : null } render_page(){ if (params['profile'] && params['msgs'] !== null){ do_render_page(); } } get_profile(user, function(data){ params['profile'] = data; render_page(); }) get_msgs(user, function(data){ params['msgs'] = data; render_page(); })
Именно эта проблема описана в После всех асинхронных вызовов и Синхронизация асинхронных вызовов. WaitSync.
Используя способ из топиков, приведенных выше, решение проблемы выглядело бы так:
var process = render_page.after('profile', 'new_msgs'); get_profile(user, process.profile); get_msgs(user, process.new_msgs );
3. Обработка ошибок
Допустим нам надо обработать ошибки или исключения, возникшие при выполнении get_profile() или get_msgs()
Для синхронного кода все довольно просто:
try{ var msgs = get_msgs(user); var profile = get_profile(user); if (profile){ render_page({'profile' : profile, 'msgs' : msgs}); }else{ redirect_to_login_page(); } }catch(e){ show_error_msg(e); }
Для асинхронных вызовов ошибки можно передавать параметром в callback или использовать отдельный callback. Exception-ы, которые могут случайно/специально возникнуть внутри get_profile() или get_msgs() нам так просто словить снаружи не получится.
4. Расширяемость
Эта проблема возникает в результате первых трех.
Допустим мы захотели последовательно получить еще список последних комментариев, последних прочитанных топиков, рейтинг. Тогда пример из первого пункта превратится в страшного монстра.
var params = {}; get_profile(user, function(profile){ params['profile'] = profile; get_msgs(user, function(msgs){ params['msgs'] = msgs; get_last_comments(user, function(comments){ params['comments'] = comments; get_last_readed_topics(user, function(topics){ params['topics'] = topics; get_rating(user, function(rating){ params['rating'] = rating; render_page(params) }) }) }) }) })
Если мы еще добавим callback для обработки ошибок, то нас скорее всего проклянут программисты, которым возможно придется разбираться в вашем коде.
К нам на помощь спешит… JSDeferred
А теперь я хочу познакомить тех, кто еще не знаком, с одной из реализаций механизма Deferred, а именно — JSDeferred. Эта библиотека позволяет работать с асинхронными вызовами как с синхронными. Как теперь будет выглядеть решение наших четырех проблем:
1. Большая вложенность кода (решение)
Сallback-и заменяются цепочками. Результат выполнения звена цепочки передается следующему звену аргументом.
var params = {}; next(function(){ return get_profile(user) }). next(function(profile){ params['profile'] = profile }). next(function(){ return get_msgs(user) }). next(function(msgs){ params['msgs'] = msgs; render_page(params); })
2. Параллельные вызовы (решение)
Звено next вызовется только после того, как все запросы в parallel вернут результаты. Аргументом передастся массив результатов выполнения parallel.
parallel([ get_profile(user), get_msgs(user) ]). next(function(params){ render_page({'profile' : params[0], 'msgs' : params[1]}) // могло быть так // render_page.apply(this, params); })
3. Обработка ошибок (решение)
При возникновении exception-а в звене — следующим будет вызвано ближайшее по порядку движения звено error. Параметром передается сообщение, брошенное через throw;
var params = {}; next(function(){ return get_profile(user) }). error(function(e){ redirect_to_login_page(); }). next(function(profile){ params['profile'] = profile }). next(function(){ return get_msgs(user) }). error(function(e){ show_error_msg(e); }). next(function(msgs){ params['msgs'] = msgs; render_page(params); })
4. Расширяемость (решение)
Тут все должно быть довольно понятно, чтобы добавить новый шаг обработчика — достаточно добавить новый элемент в цепочку.
Нюанс
Мы рассмотрели решение основных проблем, а теперь про один нюанс. Асинхронные функции/методы необходимо специальным способом подготовить:
1. Они должны возвращать объект Deferred
2. Для перехода дальше по цепочке — вызывать метод call() объекта Deferred
3. Для генерации ошибки — вызывать метод fail() Deferred
Полный список методов можно узнать в документации.
Т.е. модифицированная функция с использованием XmlHttpRequest будет выглядеть так:
function http_get(url) { var d = Deferred(); var xhr = new XmlHttpRequest(); xhr.open("GET", url, true); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.status==200){ d.call(xhr.responseText); } else { d.fail(xhr.statusText); } } xhr.send(null); return d; }
Вывод
Для одиночных AJAX запросов полезность Deferred довольно сомнительна, но если вы используете много взаимосвязанных асинхронных вызовов, или у проекта есть перспектива разрастись и усложниться, то имеет смысл обратить внимание на Deferred. Это очень мощный механизм, с помощью которого можно строить большие цепочки с комбинированными параллельными/последовательными выполнениями звеньев, обработкой ошибок и это все с читабельным кодом.
Рекомендуемая литература
1. JSDeferred on github
2. Страница проекта JSDeferred
3. Вложенные асинхронные вызовы. Объект Deferred в деталях
4. dojo.Deferred
5. JQuery Deferred/JSDeferred

