Но для начала попробуем определить эти самые проблемы, с которыми мы сталкиваемся при работе с асинхронными вызовами.
А для опытов выберем себе пример*, в котором нужно запросить профиль пользователя, новые сообщения и только после этого отобразить страницу. С использованием синхронного выполнения решение было бы таким:
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['profile'] = profile;
console.log(result)
})
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['profile'] = profile;
console.log(result)
})
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
Источник: Хабрахабр - JavaScript
Оригинальная страница: Асинхронная синхронность. JSDeferred
Комментариев нет:
Отправить комментарий