Заметки за февраль 2012 года

Градиенты в Photoshop и в браузере

Для меня оказалось откровением то, что градиенты, нарисованные в Photoshop и генерируемые браузером, сильно различаются.

Сравнение градиентов с разными параметрами

По умолчанию параметр smoothness всегда устанавливается в 100%. Но чтобы градиент стал таким же, каким его отобразит браузер, этот параметр нужно установить в 0.

Настройки градиента в Photoshop

Об этой особенности рассказал Олег Мохов в своем докладе на UWDC2012.

PS. В комментариях добавил иллюстрации стыкующихся градиентов.

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

Очередь с одновременным выполнением нескольких задач

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

Модуль использует Backbone.Events для оповещения о ходе выполнения задач и несколько полезных функций из Underscore. Экспорт объекта выполняется через мой фреймворк.

/**
 * Queue for simultaneous task execution.
 * Execution method MUST return the promise object.
 *
 * @param limit {Integer} number of simultaneous tasks
 * @event schedule
 * @event before
 * @event after
 */
(function () {

    var Task = function (obj, execMethod) {
        _.extend(this, {
            id: _.uniqueId("queueitem-"),
            obj: obj,
            execMethod: execMethod,
            active: false
        });
    };

    _.extend(Task.prototype, {
        run: function () {
            var func, value;

            this.active = true;

            func = this.obj[this.execMethod];
            if (_.isFunction(func)) {
                value = func.call(this.obj);
            }
            // return promise object
            return value;
        }
    });

    function runTasks() {
        var activeTasks = _.filter(queue, function (task) {
            return task.active;
        });

        if (queue.length > 0 && activeTasks.length < limit) {
            // we can run another task
            var candidate = _.find(queue, function (task) {
                return !task.active;
            });

            if (candidate) {
                Q.trigger("before", candidate.obj);
                var taskDfd = candidate.run();
                Q.trigger("after", candidate.obj, taskDfd);
                if (taskDfd) {
                    taskDfd.always(function () {
                        var i, id = candidate.id;
                        for (i = 0; i < queue.length; i++) {
                            if (queue[i].id === id) {
                                queue.splice(i, 1);
                                break;
                            }
                        }
                        runTasks();
                    });
                }
                // check tasks one more time
                setTimeout(runTasks, 500);
            }
        }
    }

    var queue, limit;

    var Q = _.extend({
        init: function (opts) {
            queue = [];
            limit = opts.limit;
        },
        schedule: function (obj, execMethod) {
            var task = new Task(obj, execMethod);
            if (queue) {
                queue.push(task);
                Q.trigger("schedule", obj);
                runTasks();
            }
        }
    }, Backbone.Events);

    App.namespace("App.Queue", Q);

}());

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

App.Queue.init({limit: 5});

var message, i;
for (i = 0; i < 20; i += 1) {
    message = new Message("text " + i);
    App.Queue.schedule(message, "delivery");
}

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

После добавления задания в очередь генерируется событие schedule, а до и после запуска задачи — before и after соответственно. В событии after в качестве параметра передается promise-объект задачи. Через него можно так же отслеживать окончание её выполнения в другом модуле.

App.Queue.on("after", function (messagePromise) {
    messagePromise.done(function () {
        console.log("message " + this.get("id") + " was delivered");
    });
    messagePromise.fail(function () {
        console.log("delivering message " + this.get("id") + " was failed");
    });
});

Контекстом во всех событиях выступает объект, который ставился в очередь.

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

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