Заметки с тегом «deferred»

Боремся с большой вложенностью анонимных колбеков

В Node.js почти все методы библиотек, работающих с внешними данными, выполняются асинхронно. Результат возвращается либо в виде событий, либо в виде вызова указанного колбека. Разработчик тут же не задумываясь использует анонимную функцию в качестве такого колбека, а затем ещё одну и ещё… В результате, достаточно сложный участок кода оказывается ещё менее читаем.

Вот несколько советов, как улучшить читаемость такого кода.

Декомпозиция

Самым очевидным решением будет использовать в качестве колбеков не анонимные функции, а именованные. Да, простая декомпозиция может значительно повысить читаемость кода.

Возможно, в именованные функции стоит вынести только бизнесс-логику, а обработку ошибок от предыдущей функции оставить в анонимном колбеке.

Step

Когда столкнулся с тем, что основная часть бизнесс-логики у меня оказывалась глубоко закопана в «ифах» и колбеках, то первой, что попалось мне на глаза, оказалась библиотека step.

Она оказалась чрезвычайно проста в использовании и дружелюбна к другим библиотекам, использующим принцип «первым аргументом колбека всегда идёт объект ошибки».

var step = require("step");

function findRecentPostsOfActiveUsers(callback) {
  step(
    // получаем список активных пользователей
    function () {
      var nextStep = this;
      User.find({active: true}, nextStep);
    },
    // получаем последнюю публикацию каждого из ранее найденных пользователей
    function (err, users) {
      if (err) throw err;
      var group = this.group();
      users.forEach(function (user) {
        Post.findOne({userId: user.id})
          .sort({date: -1})
          .exec(group());
      });
    },
    // на последнем шаге передаем найденые публикации в колбек
    // function (err, posts) { … }
    callback
  );
}

Все выбрасываемые исключения отлавливаются и передаются в качестве ошибки следующему «шагу».

Внутри каждого шага можно организовать очередь или параллельное исполнение других асинхронных функций.

Async

Библиотека async во многом аналогична step, в части последовательного выполнения указанных функций и передачи возвращаемого результата на следующий шаг. Однако, помимо этого, там реализована масса других полезных техник для управления порядком выполнения кода.

When

Отдельно хочу отметить библиотеку when, в которой реализован немного другой подход, чем в step и async. Управление последовательностью выполнения кода осуществляются на базе отложенных объектов.

Так как отложенный объект может только один раз изменить своё состояние, то переход с шага на шаг можно осуществлять, например, по таймауту не боясь, что колбеки потом будут будут вызваны несколько раз.

В примерах и документации подробно описаны аспекты применения этой библиотеки.

Замечание

Описанные библиотеки можно и нужно применять не только на сервере, но и в браузере.

Комментарии к заметке: 4

Конкурирующие асинхронные запросы

Никогда нельзя предсказать в какой последовательности будут отрабатываться асинхронные запросы. Зачастую важен только ответ на самый последний запрос, а предыдущие запросы можно просто проигнорировать.

var concurrent = (function () {
    var concurrentRequests = {};
    return function (opts, cache) {
        // вместо jqXHR функция будет возвращать promise-объект
        var deferred = $.Deferred(), promise = deferred.promise();

        // сохраняем колбеки, если они есть
        promise.done(opts.success || $.noop);
        promise.fail(opts.error || $.noop);
        promise.always(opts.complete || $.noop);

        // удаляем колбеки из параметров,
        // так как cacheable не обрабатывает их
        delete opts.success;
        delete opts.error;
        delete opts.complete;

        // таймстемп запроса.
        // ответы с таймстемпом меньше текущего игнорируются
        var requestId = concurrentRequests[opts.url] = $.now();

        // кеширующий или обычный запрос
        (cache ? cacheable : $.ajax)(opts)
            .done(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.resolveWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            })
            .fail(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.rejectWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            });

        return promise;
    }
}());

В паре с функций cacheable получилась достаточно универсальная замена традиционной функции $.ajax.

$(".list").each(function () {
    var list = $(this);
    list.on("click", ".list__page", function () {
        concurent({
            url: "/list/",
            data: {page: $.tim($(this).text())},
            dataType: "html"
        }, true).done(function (markup) {
            list.html(markup);
        });
    });
});
Оставте свой комментарий

Кешируем ассинхронные запросы

Когда на одной странице несколько ajax-запросов выполняются с одними и теми же параметрами и допустимо закешировать результат, то можно сделать это так:

var subscribers = {};

function getSubscribers(id) {
    var dfd = $.Deferred(), promise = dfd.promise();
    if (typeof subscribers[id] !== "undefined") {
        subscribers[id].done(function (count) {
            dfd.resolveWith(promise, [count]);
        });
    } else {
        subscribers[id] = promise;
        $.ajax({
            url: "/subscribers",
            type: "get",
            dataType: "text",
            data: {
                id: id
            }
        })
        .done(function (count) {
            dfd.resolveWith(promise, [count]);
        });
    }
    return promise;
}

Функция вернет promise-объект, который будет разрешен, когда будет известно количество подписчиков.

Если во время запроса на сервер потребуется еще раз получить количество подписчиков, то в этом случае новый ajax-запрос не будет создаваться. Мы подписываемся на результат уже запущенного запроса и ожидаем его успешного завершения.

Все последующие вызовы функции будут возвращать закешированный результат.

Обновление: Пересмотрел код и понял, что его можно оптимизировать, избавившись от промежуточного отложенного объекта. Заодно обобщил функцию, чтобы её можно было использовать для разных запросов.

var cacheable = (function () {
    var promises = {};
    return function (opts) {
        var key = $.param({
                url: opts.url || "",
                type: (opts.type || "get").toLowerCase(),
                data: opts.data || {}
            });
        return promises[key] ? promises[key] :
                (promises[key] = $.ajax(opts).promise());
    };
}());

cacheable({
    url: "/subscribers",
    type: "get",
    dataType: "text",
    data: {
        id: id
    }
});
Комментарии к заметке: 3

Скрываем логику проверки ответа асинхронного запроса

С помощью Deferred Object легко скрыть логику проверки ответа асинхронного запроса.

Считаем, что запрос отработан успешно, если с сервера вернулся JSON и поле status в объект установлено в значение ok. Если там оказалось другое значение, то запрос завершился с ошибкой.

Из метода возвращаем не Deferred XHR, а новый экземпляр обычного отложенного объекта, который в дальнейшем будем «реджектить» или «резолвить».

function subscribe(objectId) {
    var dfd = $.Deferred(),
        p = dfd.promise();

    $.ajax({
        url: "/subscribe",
        type: "post",
        data: {
            objectId: objectId
        }
    }).done(function (data) {
        if (data) {
            if (data.status == "ok") {
                dfd.resolveWith(p);
            } else {
                dfd.rejectWith(p, [data.status]);
            }
        } else {
            dfd.rejectWith(p, ["wrong response"]);
        }
    }).fail(function () {
        dfd.rejectWith(p, ["communication error"]);
    });

    return p;
}

Так как логика обработки ответа отделена от логики приложения, то эта часть кода — хороший кандидат на выделение её в общий для всего приложения обработчик.

Оставте свой комментарий

Дизайн приложения с применением Deferred Object

В заметке Deferred Object я уже приводил пример приложения, построенного из отдельных компонент и связанных между собой с помощью отложенных объектов. Сейчас я еще раз хочу акцентироваться на некоторых аспектах.

Promise object

Состояние отложенного объекта должен изменять только тот, кто его создал.

Ранее я писал:

Так как в общем случае deferred object предполагает двустороннюю коммуникацию, то ничего не мешает отменить выбор извне.

Стоит признать, что это не улучшит архитектуру приложения, а только добавит жестких связей между компонентами. Так поступать не стоит.

Все остальные компоненты, заинтересованные в изменении состояния, могут только подписываться на это изменение. Т.е. замыкание, в котором создается отложенный объект, должно возвращать promise object.

В том примере нужно исправить функцию buildSelect.

function buildSelect() {
    var select = $.Deferred();

    ...

    // возвращаем отложенный объект селектора
    return select.promise();
}

В метод promise() можно передать в качестве параметра другой объект, который будет расширен методами promise-объекта.

function createJob() {
    var dfd = $.Deferred(), value = 0, t, me;

    function schedule() {
        return setTimeout(updateValue, 500);
    }

    function updateValue() {
        value = value + 10 * Math.random();

        dfd.notifyWith(me, [value > 100 ? 100 : value]);

        if (value >= 100) {
            if (value - 100 > 5) {
                dfd.resolveWith(me);
            } else {
                dfd.rejectWith(me);
            }
            t = 0;
        } else {
            t = schedule();
        }
    }

    me = dfd.promise({
        start: function () {
            if (!t) {
                t = schedule();
            }
        },
        stop: function () {
            if (t) {
                clearTimeout(t);
                t = 0;
            }
        }
    });

    return me;
}

$(function () {

    var job = createJob();

    job.done(function () {
        alert("done");
    }).fail(function () {
        alert("fail");
    }).progress(function (value) {
        $(".progress-gauge").width(value + "%");
    }).always(function () {
        $(".btn-start, .btn-stop").addClass("hidden");
    });

    $(".btn-start").on("click", function () {
        job.start();
    });

    $(".btn-stop").on("click", function () {
        job.stop();
    });

});

Объект job, помимо методов наблюдения за процессом выполнения и результатом завершения работы, имеет метод запуска задачи и метод остановки задачи. Вся логика расчета скрыта внутри одного замыкания, а работа с GUI сконцентрирована в другом.

Пример на jsfiddle.

Контекст

Отложенный объект имеет по паре методов для изменения состояния. Например, resolve и resolveWith, reject и rejectWith. Обе пары методов позволяют передать подписчикам какие-либо параметры. Однако методы с суффиксом with позволяют указать контекст вызова (это обязательный параметр методов), а без суффикса with вызывают подписчиков в контексте отложенного объекта.

Тут возникает некоторое противоречие в дизайне. Сначала мы скрываем от внешнего мира исходный отложенный объект, а потом сами же его и показываем. Лучшим всего вызывать подписчиков в контексте того объекта, который был создан в замыкании.

Инверсия управления

Чтобы грамотно применять отложенные объекты нужно понимать принцип инверсии управления. Мы не можем управлять состоянием отложенного объекта. Он сам вызывает наши функции, когда его состояние меняется. Нам лишь остается реализовать некие действия, связанные с изменением состояния.

Комментарии к заметке: 2