Заметки с тегом «pattern» (страница 2)

Оповещение модулей через 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

Возвращаясь к проверке типа данных

Об альтернативе typeof я уже писал в заметке «Проверка типа данных в JavaScript». А недавно на просторах интернета нашел ещё один аналогичный вариант такой функции.

var toType = function (obj) {
    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}

Стоить только заметить, что он примерно в 2 раза медленнее, чем вариант со slice . Всё-таки регулярные выражения не спасает даже кеширование.

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

Улучшенный namespace

Для определения модулей и констант приложения на JavaScript я люблю применять функцию аналогичную namespace из библиотеки YUI3 . Она возвращает объект, который соответствует указанному пути или создает новый, если таковой не нашелся. Ценность этой функции в том, что можно не заботиться о том, что какая-то многоуровневая вложенность будет испорчена. Так же она позволяет определять поля в любом порядке.

На практике типовое использование этой функции сводится к получению (или созданию) объекта и тут же заполнению его какими-то полями. Я решил объединить эти действия в один вызов. Так как в этом случае не требуется объявлять несколько модулей, то этот функционал был удален вообще.


(function (window, undefined) {

    var NS = "APP",
        app = window[NS] = window[NS] || {};

    /**
     * Get or create namespace for module
     * @param    ns        {String}    namespace
     * @param    origin    {Object}    initial object (optional)
     * @return             {Object}   
     */
    app.namespace = app["namespace"] || function (ns, origin) {
        var i, obj = window[NS], nsParts = ns.split('.');
        for (i = (nsParts[0] === NS) ? 1 : 0; i < nsParts.length; i++) {
            obj[nsParts[i]] = obj[nsParts[i]] || (i === nsParts.length - 1 && origin) || {};
            obj = obj[nsParts[i]];
        }
        return obj;
    };

}(window));

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

Её можно вызывать с одним строковым параметром

APP.namespace("APP.Module1");

или двумя параметрами — строковым и объектом, все поля которого будут перенесены в целевой namespace.

APP.namespace("APP.Module2", {
    method1: function () {
    },
    FIELD: "test"
});

Для названий можно использовать укороченный синтаксис и опускать базовое имя. Так вызовы APP.namespace("Module1") и APP.namespace("APP.Module1") будут эквивалентны.

Такую функцию удобно переносить из проекта в проект. Достаточно поменять только значение константы NS.

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