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

    В последнее время на хабре появилось несколько статей про работу с асинхронными вызовами (После всех асинхронных вызовов, Синхронизация асинхронных вызовов. 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
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

      +2
      Какой исходных код этих ф-ций get_profile и get_msgs?
      parallel([
          get_profile(user),
          get_msgs(user)
      ]).
      

      Согласно вашему прошлому топику он достаточно страшный…
        0
        возможно get_profile выглядела бы так:
        function get_profile(user){
        
            var d = Deferred();
            
            if (!conn.connected()) {
                d.fail("Connection error " + conn.connectErrno + ": " + conn.connectError);
            }else{
                conn.query("SELECT * FROM users where user="+user, function (err, res){
                    if (err) {
                        d.fail(err);
                    }else{
                        d.call(res.fetchFirst())
                    }
                })
            }
            
            return d;
        }
        
          –2
          да, не очень красиво.
            +1
            Нееее, если уж делать врап, то он должен быть сам по себе, без диких конструкций внутри.
            не совсем понимаю зачем делать обьект Deffered в таком виде, если можно снаружи легко проверить instanceof?
              0
              Если предварительно сделать врап для объекта conn, то получится
              function get_profile(user){
                  var d = Deferred();
              
                  conn.query("SELECT * FROM users where user="+user)
                      .error(function(e){d.fail(e)})
                      .next(function(data){d.call(data)});
              
                  return d;
              }
              
                0
                коли уж conn.query() возвращает deferred, то должно быть так:
                function get_profile(user){
                     return conn.query("SELECT * FROM users where user="+user)
                        .error(function(e){d.fail(e)})
                        .next(function(data){d.call(data)});
                }
                
                  +2
                  Ошибся, вот так:
                  function get_profile(user){
                       return conn.query("SELECT * FROM users where user="+user);
                  }
                  

                  И обработчики уже вне этой функции навешивать
                    0
                    Вы правы
                  –2
                  Ну вот. А теперь напишем функцию, которая будет делать врап за вас.

                  // simplified
                  function wrap (fn) {
                     return function () {
                        var d = Deferred(); 
                        fn();
                        return d;
                     }
                  }
                  


                  Вот это прибилизтельно то, что я делаю =)
                    0
                    Только вот в fn надо передать d, чтобы на нём вызвать колл/еррбэки
                      0
                      Ну блин, разумеется, но способы передачи разные есть, я и упростил. Ее можно и в параметры подмешать, и к самой функции прицепить (если параметры менять нельзя).
            0
            Мне лично кажется что в случае большой вложенности нужно ее уменьшать: это узкое место, не проще на сервере собрать один готовый объект и прислать его? Т.е. в первом случае вернуть объект user, в котором есть профиль, сообщения, последние комменты и тд. Зачем столько раз обращаться с запросами к серверу, если нет никакой зависимости между этими сущностями? Запрашиваем: верни user с комментами и профилем, сервер формирует одним json объектом ответ, нужен только профиль, пожалуйста.
              +1
              а что, если это асинхронный запрос к бд?
                –1
                Вы видимо про серверный js говорите, тогда посоветую использовать синхронный коннектор к базе) Либо библиотеку step https://github.com/creationix/step
                  +1
                  синхронный на node.js? зачем?
                    –2
                    во первых параллельная организация работы все равно решается с помощью воркеров, подключение к базе и отключение, имхо, должно быть синхронным. Время затраченное на подключение к локальной базе ничтожно, и вешать обработчик на это глупо — он лишь запутывает код. А вот запросы можно дергать как синхронно, так и асинхронно. Все зависит от времени и места.
                      0
                      подключение к базе осуществляется один раз при старте сервера (ну или несколько раз).
                      а запросы выполняются много раз и, я так понял, должны быть асинхронными
                        0
                        Все зависит от стиля программирования и решаемой проблемы. Я считаю что злоупотребление такими обертками подталкивает программиста к процедурному стилю. Как и излишняя вложенность калбеков, если такое присутствует, надо получше продумывать архитектуру приложения\класса. К вопросу о синхронности запросов: Если от какого-то запроса зависит дальнейшая работа приложения (например логин к системе), но асинхронность тут не нужна. Если это обновление какких-то данных, то однозначно асинхронность. Если это сохранение: тут спорно, кто-то блокирует работу пока оно не завершится, кто-то сохраняет на лету… каждый случай требует своего решения
                          +1
                          странно. казалось бы, при чём тут такие обёртки к стилю программирования. что говнокод, что хороший код можно написать и с асинхронным подходом и с синхронным.
                            0
                            Слово «подталкивает» ключевое. Говнокод и процедурный стиль это разные вещи. А зачем забивать все одним микроскопом, если можно пользоваться двумя?=) Я считаю не нужно уходить в секту синхронности или асинхронности, всему свое место. Если у вас логин блокирует всю программу, то зачем точку входа запихивать в калбек, это не наглядно. Хотя личное дело каждого)
                              0
                              весь смысл асинхронного подхода в том, чтобы делать асинхронные запросы и что-то делать когда они обработаны, и что-то делать в промежутке между этими событиями (или дать другим делать что-то). Традиционные треды и дочерние процессы работают по-другому. Там и подход другой.
                +2
                А что, если это на сервере выполняется? :-)
                +2
                Ну что вы все время пару решений мусолите?
                Подумали бы лучше как заврапить типичную проблему для формы:
                есть поле, которое верифицируется аяксом и есть сабмит формы, которое требует верифицированного поля
                обычно вешается на blur поля запрос, но некоторые браузеры подставляют значение в поле, так что неплохо бы верифицировать непустое поле на сабмите.
                В результате на сабмите пара флагов на верификацию ( старт, успешно), а в запросах на последнюю флаг на сабмит формы ( что бы таки сделать сабмит если верификация прошла ).

                Что происходит если таких полей в форме несколько представить можно, но страшно :)
                  0
                  отправить на сервак аяксом для валидациии и баста
                    0
                    отправлено — надо ждать
                    не отправлено — надо отправить
                    получено — сабмитить форму

                    бастой тут и не пахнет
                  0
                  если обработчик упадёт с исключением — фейл будет вызван или мы так и не узнаем, что всё упало?
                    –1
                    если исключение возникает внутри звена, например .next, то управление переходит в ближайший по порядку .error, где можно получить само сообщение исключения.
                      0
                      тогда зачем «Для генерации ошибки — вызывать метод fail() Deferred» если можно просто бросить исключение?
                        0
                        вообще, внутренние try{}catch{} это, по-моему, лишнее. По крайней мере, отлов должен быть опциональным.
                    –1

                  Only users with full accounts can post comments. Log in, please.