Заметки в категории «JavaScript» (страница 9)

Дизайн приложения с применением 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

Самый маленький плагин для jQuery

English version and project repository.

Простейший плагин будет состоять только из одного выражения return this.

$.fn.noop = function () {
    return this;
};

Профит от этого плагина не очевиден на первый взгляд. Если вызвать его, как вызываются тысячи других, то в результате получим ту же самую не модифицированную коллекцию элементов.

$("div.title").noop().doSomething();

Практическая польза от него будет, когда плагин должен вызываться неявно.

$("div.title")[isVisible() ? "fadeTo" : "noop"](333, 0).doSomething();

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

На написание заметки вдохновил твит:

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

Оповещение модулей через jQuery.Callbacks

Обмен событиями между отдельными компонентами программы можно построить различными способами. В зависимости от потребностей это можно сделать:

  1. через интерфейс Observable или произвольные DOM-события;

    В основе этого подходе лежит то, что одна из компонент обладает информацией о другой. Например, имеет ссылку на экземпляр класса или корневой элемент виджета.

  2. через глобальный объект;

    Экземпляры компонент ничего не знают друг о друге. Все события пересылаются через один и тот же объект.

  3. через объекты событий.

    В этом случае экземпляры компонент так же ничего не знаю друг о друге. А каждое событие имеет отдельный объект в глобальном пространстве имен.

Интерфейс Observable удобен тем, что события одного экземпляра ни как не пересекаются с событиями другого экземпляра одной и той же компоненты.


$("div.primary div.widget").on("update", function () {
    alert("update in primary block");
}

$("div.secondary div.widget").on("update", function () {
    alert("update in secondary block");
}

Два экземпляра одного и того же виджета, но расположенных в разных блоках генерируют разные события «update».

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

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


var Topic = (function () {
    var topics = {};
    return function (name) {
        var callbacks, method, topic;
        if (!name) {
            throw new Error("topic must be specified");
        }
        topic = topics[name];
        if (!topic) {
            callbacks = jQuery.Callbacks();
            topic = {
                publish: callbacks.fire,
                subscribe: callbacks.add,
                unsubscribe: callbacks.remove
            };
            topics[name] = topic;
        }
        return topic;
    };
}());

Замыкание содержит в себе список всех зарегистрированных событий. Для каждого из них создается менеджер очереди колбеков jQuery.Callbacks , который регистрирует и вызывает их.

Наружу возвращается объект с методами экземпляра jQuery.Callbacks, но именованными в соответствии с действиями, за которые они отвечают: subscribe, unsubscribe и publish.


Topic("archiveAdd").subscribe(function (id) {
    console.log("add file with id = " + id + " to archive");
});
Topic("archiveCreate").subscribe(function () {
    console.log("start archiving");
});

И в другой компоненте:


$(root).on("click", "li.file button.add", function () {
    Topic("archiveAdd").publish($(this).data("fileId"));
});
$(root).on("click", "button.create", function () {
    Topic("archiveCreate").publish();
});

В некоторых случаях может быть полезным для каждого события создать свой отдельный объект и использовать ссылку на него в компонентах


var EventObject = function () {
    var callbacks = jQuery.Callbacks();
    return {
        publish: callbacks.fire,
        subscribe: callbacks.add,
        unsubscribe: callbacks.remove
    };
};

Такая реализация может быть близка сторонникам процедурных языков, но мне кажется чужеродным для функционального стиля JavaScript.

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

Очередь функций обратного вызова jQuery.Callbacks

В jQuery 1.7 появился новый объект для создания и управления очередью колбеков – jQuery.Callbacks. Он был предложен на рассмотрение команде разработчиков еще 6 месяцев назад. Менеджер было успешно опробован и пошел в релиз. На его основе был переписан jQuery.Deferred, который в свою очередь лежит в основе системы колбеков jQuery.ajax.

Конструктор jQuery.Callbacks можно вызывать как с оператором new, так и без него. В результате будет создан объект с несколькими методами: add(), remove(), fire(), disable() и др.

В параметры конструктору можно передать один или несколько флагов. От их наличия и сочетания будут меняться параметры, с которыми вызываются колбеки, и поведение очереди в целом. Рассмотрим их подробнее.

Вызов конструктора с различными флагами

Очередь, созданная без флагов, ведет себя так как если бы это была, например, очередь используемая в событиях.


var c = $.Callbacks();
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

После выполнения этого кода в консоль будет выведено:

1: foo

1: bar

2: bar

Метод add() добавляет в очередь новую функцию обратного вызова, а метод fire() по очереди вызывает все функции из очереди с указанными параметрами.

Флаг once указывает на то, что метод fire() будет выполнен только один раз для такой очереди.

var c = $.Callbacks("once");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

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

1: foo

Флаг memory предписывает запоминать последнее значение, с которым был вызван метод fire() и автоматически вызывать вновь добавляемые колбеки с этим значением.


var c = $.Callbacks("memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

Когда вызывается метод fire("foo"), то второго колбека еще нет в очереди. Но как только он будет добавлен, так сразу вызовется с параметром "foo".

1: foo

2: foo

1: bar

2: bar

Флаги можно комбинировать. Так одновременное использование once и memory даст результат аналогичный по свой сути работе jQuery.Deferred – вызов колбеков будет сделан только один раз, а новые колбеки сразу будут вызываться при их добавлении.

var c = $.Callbacks("once memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

1: foo

2: foo

Если будет установлен флаг unique , то одна и та же функция обратного вызова в очередь будет добавлена только один раз.

И последний флаг stopOnFalse прерывает выполнение очереди, если какой-либо колбек вернет значение false.

Применение

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

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

Deferred Object

Термин «отложенный объект» тесно связан с событийной моделью создания компонент и модулей приложения. По сути, представляет собой интерфейс, с помощью которого можно в одном модуле подписываться на события другого модуля. Основной отличительной чертой deferred object является то, что если ожидаемое событие уже наступило (например, окончилась ajax-загрузка), то подписчики все равно будут уведомлены о результате, как только они добавят свои функции обратного вызова. В обычной событийной модели такого поведения не предполагается.

Для полного понимания приведу пример фабрики отложенных объектов Дугласа Крокфорда из его презентации «Act III: Function the Ultimate»


function make_promise() {
    var status = 'unresolved',
        outcome,
        waiting = [],
        dreading = [];

    function vouch(deed, func) {
        switch (status) {
            case 'unresolved':
                (deed === 'fulfilled' ? waiting : dreading).push(func);
                break;
            case deed:
                func(outcome);
                break;
        }
    }

    function resolve(deed, value) {
        if (status !== 'unresolved') {
            throw new Error('The promise has already been resolved:' + status);
        }
        status = deed;
        outcome = value;
        (deed == 'fulfilled' ? waiting : dreading)
            .forEach(function (func) {
                try {
                    func(outcome);
                } catch (ignore) {}
            });
        waiting = null;
        dreading = null;
    }

    return {
        when: function (func) {
            vouch('fulfilled', func);
        },
        fail: function (func) {
            vouch('smashed', func);
        },
        fulfill: function (value) {
            resolve('fulfilled', value);
        },
        smash: function (string) {
            resolve('smashed', string);
        },
        status: function () {
            return status;
        }
    };
}

Состояние объекта можно изменить только один раз каким-либо методом: fulfill или smash . После этого будет выполнены функции, которые были зарегистрированы методами when и fail, соответственно.

Стоит заметить, что отложенные объекты применимы только к разовым событиям. После того, как объект был установлен в какое-то состояние, изменить его уже нельзя.

Эту концепцию команда разработчиков jQuery реализовала в своем фреймворке и начала использовать при работе с AJAX. Далее я буду использовать именно эту реализацию.

Рассмотрим пример приложения, с использованием отложенных объектов: «По клику на элемент появляется селектор с мероприятиями, содержимое которого загружается из внешнего сервиса. Когда пользователь выбирает мероприятие, оно добавляется в список».

Начнем с функции, отслеживающей клики. Она ничего не знает о селекторе и списке кроме того, что они существуют и поддерживают интерфейс jQuery.Deferred.


$(function () {
    // отслеживаем нажатие элемента
    $("span.show-selector").bind("click", function () {
        var ele = $(this), select;
        // ничего не делаем, если уже есть блок с селектором
        if (ele.hasClass("busy")) {
            return;
        }
        ele.addClass("busy");

        // создаем селектор
        select = buildSelect();
        // передаем отложенный объект селектора в функцию обновления списка
        updateList(select);

        // как только в селекторе было выбрано мероприятие снимаем флаг
        select.always(function () {
            ele.removeClass("busy");
        });
    });
});

Функция, создающая селектор знает, что список мероприятия нужно откуда-то получить, но не вдается в реализацию этого процесса. Она рисует селектор и возвращает отложенный объект, который изменит свой статус после того как пользователь сделает свой выбор. Статус так же изменится, если при получении списка возникнет ошибка.


function buildSelect() {

    var select = $.Deferred(),
        fetcher = fetchEvents();

    // когда данные о мероприятиях будут получены, рисуем селектор
    fetcher.done(function (data) {
        var ele = [], cache = {};

        $.each(data.query.results.event, function (index, upcomingEvent) {
            ele.push('<option value="' + upcomingEvent.id + '">');
            ele.push(upcomingEvent.name);
            ele.push('</option>');
            cache[upcomingEvent.id] = upcomingEvent;
        });

        $("p.title").append('<div id="upcoming-events">' +
            '<select>' + ele.join('') + '</select>' +
            '<button class="btn-done">Done</button>' +
            '<button class="btn-cancel">Cancel</button>' +
            '</div>');

        // отслеживаем нажания на кнопки, чтобы выбрать
        // мероприятие или отменить выбор
        $("#upcoming-events").delegate("button", "click", function () {
            var upcomingEvent;
            if ($(this).hasClass("btn-done")) {
                // мероприятие было выбрно
                upcomingEvent = cache[$("#upcoming-events select").val()];
                // меняем состояние отложенного объекта
                // и передаем подписчикам информацию о мероприятии
                select.resolve(upcomingEvent);
            } else if ($(this).hasClass("btn-cancel")) {
                // выбор был отменен пользователем
                select.reject("user");
            }
        });

    });

    // ошибка при получении списка мероприятий
    fetcher.fail(function () {
        // отменяем выбор из-за сбоя связи с сервисом YQL
        select.reject("network");
    });

    // удаляем селектор, когда было выбрано какое-то мероприятие
    // или выбор был отменен по каким-то причинам
    select.always(function () {
        $("#upcoming-events").remove();
    });

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

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

Функция получения списка мероприятий запрашивает их через YQL . Сейчас она реализована в самом простом варианте без кеширования полученных результатов. Но благодаря тому, что используются унифицированный deferred object, а не функции обратного вызова в ajax-запросе, функциональность можно легко дополнить.


function fetchEvents() {
    // получаем список мероприятий через сервис YQL
    var jqXHR = $.ajax({
        url: "http://query.yahooapis.com/v1/public/yql",
        data: {
            q: 'select * from upcoming.events where tags="birthday"',
            format: 'json'
        },
        dataType: 'jsonp'
    });
    // возвращаем отложенный объект ajax-загрузки
    return jqXHR;
}

И наконец последняя функция, рисующая выбранное мероприятие, просто подписывается на событие выбора.


function updateList(select) {
    // в селекторе было выбрано какое-то мероприятие
    select.done(function (upcomingEvent) {
        // получаем элемент списка или создаем его
        var list = $("dl.list");
        if (!list.length) {
            list = $('<dl class="list"></dl>').appendTo("body");
        }

        // добавляем новую запись о мероприятии в список
        list.append('<dt>' + upcomingEvent.name + '</dt>' +
            '<dd>' + upcomingEvent.description + '</dd>');
    });
}

Посмотреть работу примера.

В заключении хочу сказать, что deferred object не представляет собой замену обычным событиям, а скорее является частным случаем, когда допустимы следующие особенности:

  • ожидаемое событие случается только один раз;
  • подписчики могут добавлять функции обратного вызова даже после того как событие случилось.

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

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