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

Использование setTimeout() вместо setInterval()

Функция setInterval() предназначена для вызова переданного ей колбека через равные промежутки времени. У неё есть две особенности:

  1. Сложно (а иногда попросту не возможно) заниматься отладкой такого кода из-за того, что вызовы колбека будут производиться независимо от того, в каком месте кода вы поставили точку останова. На каждый шаг отладки управление будет передаваться основному циклу, который исправно вызовет колбек о очередной раз.
  2. Если длительность выполнения колбека по каким-либо причинам превысит установленную задержку, то это автоматически приведёт к лавинообразному увеличению количества замыканий, «выполняемых одновременно». Колбек будет вызываться снова и снова через заданный промежуток времени, не дожидаясь завершения предыдущего вызова.

От этих проблем можно избавиться, если воспользоваться setTimeout() . В этом случае планирование следующего вызова будет производиться только после завершения выполнения предыдущего.


/**
 * @param {Function} callback
 * @param {Number} delay
 * @param {Object} [context]
 * @return {Function}
 */
function interval(callback, delay, context) {
    var start = new Date(), id;

    delay = delay || 0;
    context = context || this;

    (function loop() {
        var result = callback.call(context, new Date() - start);
        if (result !== false) {
            id = setTimeout(loop, delay);
        }
    }());

    return function () {
        if (id) {
            clearTimeout(id);
            id = 0;
        }
    };
}

Кроме того, мы получаем несколько способов завершить выполнение этого бесконечного цикла. interval() возвращает функцию, с помощью которой можно прерывать следующий запланированный вызов колбека. Если колбек сам вернет значение false , то выполнение цикла также прекратится.


function doStuff(time) {
    $("body").append("<p>" + time  + "</p>");
    return time < 10000;
}
var cancelFn = interval(doStuff, 1000);
$("button").on("click", cancelFn);

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

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

Шаблонирование на JavaScript

Динамическое создание и обновление блоков страницы с давних пор принято делать через свойство DOM-элемента innerHTML . Если речь идет о куске HTML, пришедшего с сервера в асинхронном запросе, то другого адекватного варианта просто нет. С другой стороны, когда с сервера получаем только данные, а разметку создаем скриптом на JS, то тут уже появляются варианты.

Различные реализации JS-движка имеют свои сильные и слабые стороны. Долгое время создание DOM-элементов через document.createElement было очень дорогой по времени операцией. Не самый лучший дизайн API требовал многословности при создании увесистой структуры из элементов с атрибутами.


var container = document.createElement("div");
var link = document.createElement("a");
link.setAttribute("href", "http://yandex.ru/");
link.setAttribute("title","Яндекс");
link.appendChild(document.createTextNode("Поиск"));
container.appendChild(link);

Этот фрагмент фактически соответствует:

<div><a href="http://yandex.ru/" title="Яндекс">Поиск</a></div>

Поэтому широкое распространение получили различные шаблонизаторы на основе замены частей строки-шаблона данным или склеивания строки из нескольких частей.

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

HTML – всегда был и остается результатом сериализации древовидной структуры документа в текстовый формат, который может легко восприниматься и редактироваться человеком. Браузер, получая его на вход из файла или через поле innerHTML, разбирает его, превращая в DOM-дерево, прежде чем примется его отображать.

Современные браузеры достаточно быстро выполняют JavaScript, чтобы дать сборке фрагментов DOM-дерева через соответствующие методы API, ещё один шанс.

Синтаксический сахар

Neil Jenkins сделал «сахарок» для создания отдельных элементов и целых иерархий – Sugared DOM. Однако его код не работает в IE из-за отличной от других движков реализации метода split() у строк. Я переделал работу в этой части кода и добавил unit-тесты.

el("div");

Элементы можно создавать с установленными атрибутами id и class , используя знакомый CSS синтаксис

el("div#id.class1.class2");

Элементу можно установить другие атрибуты. Например,

el("div#id", { tabindex: -1, title: "Контейнер" });

Дочерние элементы передаются в массиве последним аргументом

var container = el("div", [
    el("a", {"href": "http://yandex.ru/", "title": "Яндекс"}, [
        "Поиск"
    ])
]);

Производительность

Тесты показывают превосходство DOM версии во всех браузерах кроме Оперы, где разница в скорости почти не заметна и Интернет Эксплорера, который всё же быстрее работает с полем innerHTML.

Ноутбуки и настольные компьютеры в настоящее время настолько мощные, что разница в скорости создания фрагментов не так уж и важна. С другой стороны, на мобильниках и планшетах – это может быть критичным. Тесты показали, что DOM версия быстрее от 45 до 100% в WebKit браузерах (браузеры в iOS устройствах и браузер по-умолчанию в Android устройствах), и примерно одинакова с innerHTML версией в Opera Mobile.

Заключение

Sugared DOM метод имеет ряд преимуществ перед шаблонизаторами:

  • Прост в отладке (описание шаблона – это и есть исполняемый код).
  • Не нужно дополнительно искать элементы после создания фрагмента – все ссылки на готовые элементы можно получить в процессе построения дерева.
  • Не нужно беспокоиться об экранировании данных. Нулевая вероятность XSS. Текстовая стока явно преобразуется в текстовый узел DOM-дерева.
  • Нет пустых текстовых узлов из-за пробелов между тегами.
  • Среда разработки помогает отслеживать ошибки в таком шаблоне, так как он является обычным JS-скриптом.
  • Гибкость шаблонирования обеспечивается полным доступом к объектам и функциям JS.

PS. Заметка вдохновлена статьёй Building the new AJAX mail UI part 2: Better than templates, building highly dynamic web pages

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

Управляем скоростью вызова функций

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

Фреймворк Underscore предоставляет два вспомогательных метода для этих целей.


function debounce(func, wait, immediate) {
    var timeout;
    return function () {
        var context = this, args = arguments;
        var later = function () {
            timeout = null;
            if (!immediate) {
                func.apply(context, args);
            }
        };
        if (immediate && !timeout) {
            func.apply(context, args);
        }
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

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


$(window).on("resize", debounce(function () {
    console.log("resize event");
}, 1000));

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


$(window).on("resize", debounce(function () {
    console.log("resize event");
}, 1000, true));

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


function throttle(func, wait) {
    var context, args, timeout, throttling, more, result;
    var whenDone = debounce(function () {
        more = throttling = false;
    }, wait);
    return function () {
        context = this;
        args = arguments;
        var later = function () {
            timeout = null;
            if (more) {
                func.apply(context, args);
            }
            whenDone();
        };
        if (!timeout) {
            timeout = setTimeout(later, wait);
        }
        if (throttling) {
            more = true;
        } else {
            result = func.apply(context, args);
        }
        whenDone();
        throttling = true;
        return result;
    };
}

Метод throttle возвращает функцию, которая выполнит самый последний вызов в течение указанного промежутка времени.

$(window).on("resize", throttle(function () {
    console.log("resize event");
}, 1000));

При изменении размеров окна сообщение будет выводиться в консоль ровно 1 раз в секунду.

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

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

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

Улучшенный 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.

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