Как стать автором
Обновить

Асинхронная синхронность. JSDeferred

JavaScript *
В последнее время на хабре появилось несколько статей про работу с асинхронными вызовами (После всех асинхронных вызовов, Синхронизация асинхронных вызовов. WaitSync). Но при ближайшем рассмотрении область их применения довольно узка так как эти способы не решают всех проблем.
Но для начала попробуем определить эти самые проблемы, с которыми мы сталкиваемся при работе с асинхронными вызовами.

А для опытов выберем себе пример*, в котором нужно запросить профиль пользователя, новые сообщения и только после этого отобразить страницу. С использованием синхронного выполнения решение было бы таким:
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
Теги:
Хабы:
Всего голосов 48: ↑44 и ↓4 +40
Просмотры 6.1K
Комментарии Комментарии 31