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

Шаблонирование на 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 раз в секунду.

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

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

Когда на одной странице несколько 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;
}

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

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

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

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

Модуль использует 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");
    });
});

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

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