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

пятница, 4 октября 2013 г.

Создание собственных событий в JavaScript

Иногда требуется организовать бизнес-логику, основываясь на событийном механизме (шаблон Observer). Обычно это делается разово, но последнее время такие ситуации стали появляться всё чаще, и я решил создать универсальное решение, которое позволит расширять или создавать список событий для любого JavaScript-объекта.

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

//Сначала добавляем нужные методы к объекту
var x = {};
setAddEvent(x);

//Затем добавляем событие. Называем его 'event1'. На выходе получаем функцию, которую нужно будет вызывать, когда событие произойдёт
var f = x.addEvent('event1');

// Теперь добавляем обработчики события - способ стандартный (такой же, как принятый в DOM)
var y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);

// Вызываем событие
f(); // "Событие 1 произошло!" и "Событие 1 произошло! (Второй обработчик)"

// Удаляем один из обработчиков
removeEventListener('event1', y, false)

// Снова вызываем событие
f(); // "Событие 1 произошло! (Второй обработчик)"

// Удаляем последний обработчик
removeEventListener('event1', z, false);

// Вызываем событие
f(); // Ничего не выводит

// Удаляем событие
removeEvent('event1');

// Пробуем вызвать событие
try { f(); } catch(/** @type Error */ e) { console.error(e.message); } // Выведет в консоль сообщение об ошибке: "Событие 'event1' было удалено!"
Примерно так. Добавлю ещё, что функция removeEventListener возвращает значение типа boolean, означающее наличие, либо отсутствие иных обработчиков данного события в настоящий момент.

Без лишних предисловий, вот основной код функции:
/** Функция делает переданный ей объект Observer`ом, т.е. генератором событий, на которые можно подписываться и
 * отписываться. После её вызова у переданного ей объекта появляются 3 метода: <code>addEvent(name)</code> для
 * добавления событий и пара функций <code>{@link #addEventListener}</code> и
 * <code>{@link #removeEventListener}</code> для добавления и удаления обработчиков событий.
 * @param {Object} that любой объект JavaScript, который нужно сделать Observer`ом.
 * @returns {Object} переданный в качестве аргумента, обогащённый методами <code>{@link #addEvent}</code>,
 * <code>{@link #addEventListener}</code> и <code>{@link #removeEventListener}</code> объект
 * <code><em>that</em></code>. */
function setAddEvent(that) {

    var /** @type Object<Object<Function[]>>*/ events = {};

    /** Цепляет к объекту событие, на которое после этого можно будет вешать обработчики при помощи метода
     * <code>{@link #addEventListener}</code>.
     * @param {string[]} name имя события (которое потом нужно будет передавать первым параметром - type - в метод
     * <code>addEventListener</code>).
     * @returns function(string, boolean) функция, которую нужно вызывать в ответ на событие, передавая ей при
     * вызове объект события. Функция будет вызывать все слушатели по порядку, передавая им этот объект. */
    that.addEvent = function(name) {

        if (name in events)
            throw new Error('Cобытие ' + name + ' уже есть!');

        /** @type Function[] */ (
            /** @type Object<Function[]> */ events[name] = {}
        )[true] = [];
        /** @type Function[] */ events[name][false] = [];

        return function(event, isPropagation) {
            if (!(name in events)) throw new Error('Событие \'' + name + '\' было удалено!');
            if (typeof isPropagation === 'undefined') isPropagation = false;
            for (var /** @type Function[] */ listeners = events[name][isPropagation],
                     /** @type number     */ i = 0,
                     /** @type Function   */ listener;
                 listener = listeners[i++];)
                listener(event);
        };
    };

    /** Удаляет событие и всех его слушателей из объекта.
     * @param {string} name Имя события, назначенное ему ранее при вызове метода <code>{@link #addEvent}</code>.*/
    that.removeEvent = function(name) {
        if (!(name in events))
            throw new Error(
                'События ' + name + ' уже итак нет, либо оно не было создано при помощи метода \'addEvent\'!');

        delete events[name];
    };

    /** Добавляет к объекту слушателя события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>addEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {function(Event)} Параметр, переданный функции вторым - handler. */
    that.addEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            if (eventName in events)
                events[eventName][isPropagation].push(handler);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return handler;
        };
    }(that.addEventListener);

    /** Удаляет из объекта обработчик события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>removeEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {boolean} Остались ли ещё обработчики для данного события? Возврат значения <code>false</code>
     * указывает на бессмысленность дальнейшего вызова функции, возвращённой методом <code>{@link #addEvent}</code>
     * при создании данного события - пока при помощи метода <code>{@link #addEventListener}</code> не будет
     * прикреплён хотя бы один новый обработчик. */
    that.removeEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            var /** @type Function[] */ listeners,
                /** @type number     */ index;
            if (eventName in events &&
                (index = (listeners = events[eventName][isPropagation]).indexOf(handler)) !== -1)
                listeners.splice(index, 1);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return listeners.length > 0 || events[eventName][!isPropagation].length > 0;
        };
    }(that.removeEventListener);

    return that;
}

//Test
var f = setAddEvent(this).addEvent('event1'),
    y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);
f();
addEventListener('load', function(){ console.log('loaded'); }, false);
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', y, false)
        ? 'Да': 'Нет'
    ));
f();
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', z, false)
        ? 'Да': 'Нет'
    ));
f();
removeEvent('event1');
try { f(); } catch(/** @type Error */ e) { console.error(e.message); }

среда, 29 февраля 2012 г.

Встраивание поддержки стандартов в DOM-дерево

В предыдущих статьях я приводил код, позволяющий добиться частичной поддержки стандартных методов в IE6-8, используя минимум ресурсов. Но встраивание его в DOM-модель производилось пользователем вручную. В данной статье я намерен показать, как этого можно было бы избежать, сделав стандартизацию IE прозрачной.

В одном из предыдущих постов я описал функцию, которая добавляет метод addEventListener к любому DOM-объекту при помощи нестандартного IE`шного метода attachEvent (в последующих постах я привёл для неё ряд улучшений - 1, 2). И всё бы хорошо, но сама по себе эта функция, очевидно, проблемы не решает, ведь теперь её нужно вызывать для всех объектов DOM-дерева, с которыми работает сценарий.

Можно было бы, конечно, вставлять эти вызовы в основной код, корректно отрабатывающий во всех браузерах, кроме IE6-8, но это его замусорит. Я уже не раз писал, что в современных проектах код часто итак достаточно сложен, так что отвлекаться при его написании ещё и на нестандартные браузеры часто слишком накладно как с позиции трудоёмкости написания, так и с позиции трудоёмкости его последующего изменения при отладке и расширении.

Так что я решил пойти несколько другим путём.

Я написал основной код на стандартном JavaScript`е строго в рамках стандартов, отладил всё в стандартных браузерах - IE9, а так же в последних версиях Chrome, Safari, Opera и FireFox, и лишь потом взялся за IE6-8, создав для них отдельный файл сценария в проекте. Добавление этого сценария можно произвести очень удобно - IE умеет понимать комментарии специального вида, которые диктуют ему различное поведение в отличие от его различных версий. Я написал так:

<!--[if lt IE 9]><script type="text/javascript" src="/_scripts/_lib/patch.js"></script><![endif]-->
Очень удобно, что все остальные браузеры считают этот код простым комментарием и не вставляют его, а IE 8`й и более ранних версий этот код вставляет и выполняет. Этот "комментарий" я вставил до объявления всех остальных сценариев, добившись, таким образом, что бы у меня была возможность в этом сценарии именно нужным образом подготовить браузер к выполнению стандартного кода, следующего дальше.

Итак, теперь осталось решить, как ранее уже описанную функцию setAddEventListener вызывать для всех DOM-объектов, что бы в нужный момент у них можно было вызвать стандартный метод addEventListener.

Легко понять, что попытка решить эту задачу в общем слишком глобальна, ведь модель DOM по сути представляет собой в большей степени коллекцию интерфейсов, а не конструкторов, как многие ошибочно полагают. В действительности у нас нет, например, конструктора Node и конструктора HTMLElement в браузерной среде - у нас есть только фабрика document, имеющая методы, которые возвращают объекты, соответствующие этим интерфейсам. Так что бесполезно было бы пытаться добавлять функционал к прототипам этих интерфейсов - они ни на что не влияют, а зачастую и вообще не существуют в браузерном окружении. Если мы попытаемся добавить стандартный, но отсутствующий в IE6-8 метод к Node.prototype или HTMLElement.prototype, надеясь, что данный метод появится у всех HTMLElement`ов, мы неминуемо будем разочарованы их отсутствием, а то и вообще получим ошибку при обращении к несуществующему объекту. Так что добавлением стандартного функционала надо заниматься на уровне фабрики - т.е. объекта document.

Однако сказать это проще, чем сделать. Ведь способов получить DOM-объекты в JavaScript масса и учесть все возможные их комбинации не представляется возможным! Вот, допустим, мы переопределим стандартный и наиболее часто встречающийся метод document.getElementById примерно так:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result);
        return result;
    }
}();
Вроде бы, всё замечательно, но это, ведь, не единственный способ получения элемента. Вот вернули мы этим нашим методом в стандартный сценарий element, а в этом стандартном сценарии производится вызов свойства parentNode или nextSibling, и на него уже производится попытка повесить обработчик события стандартным образом - с этим как? Ведь, это же свойства, а не функции, и в IE6-8 у нас нет методов определения их в качестве properties - ни ECMAScript5`овских Object.defineProperties c Object.defineProperty, ни даже FireFox`овских нестандартных (и кстати уже deprecated) __defineGetter__ с __defineSetter__`ом. Так что мы не можем обработать их вызовы. Можем, конечно, написать так:
setAddEventListener(result)(result.parentNode)(result.nextSibling);
, но если в стандартном коде свойство parentNode будет вызвано несколько раз - и этот объект прописывать, а остальные?.. В общем, реальных комбинаций может быть очень много - это, конечно, конечное множество, но полноценный качественный алгоритм, который переберёт все возможные варианты, неминуемо серьёзно затормозит работу браузера, так что это решение не приемлемо.

Так что, думаю, будет не рентабельно вершить мир во всём мире, создавая универсальное решение - гораздо продуктивнее отталкиваться от конкретики стандартного сценария, которому нужна поддержка ограниченного стандартного функционала и обеспечить её здесь и сейчас. В моём случае мне вполне хватало одноразового использования ссылки parentNode для возвращаемого методом document.getElementById элемента, так что я остановился на вполне достаточном для меня варианте:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result)(result.parentNode);
        return result;
    }
}();
Кроме того, мне был нужен слушатель для элемента, возвращаемого конструктором Image, который по аналогии был реализован так же сходным образом, лишь с незначительными изменениями:
Image = function (){
    var src = Image;
    return function(){
        var /** @type HTMLImageElement */ result = new src;
        setAddEventListener(result);
        return result;
    }
}();

Улучшение метода setAddEventListener

Активное применение описанноего в одном из предыдущих постов метода setAddEventListener для придания браузерам IE6-8 стандартного W3C DOM`овского метода addEventListener позволило выявить у первого некоторые недостатки. О крупных недостатках я напишу отдельно, но о паре мелких улучшений давайте поговорим сейчас.

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

Вот как теперь можно объявив эту функцию, сразу же в том же выражении элегантно вызвать её и для объекта window и для объекта document:

var setAddEventListener = function f(that) {
    if (!('addEventListener' in that) && 'attachEvent' in that) {
        //реализация
        that.addEventListener = function(eventName, handler) {
            //реализация
            return handler;
        };
        that.removeEventListener = function(eventName, handler) {
            //реализация
            return handler;
        };
    }
    return f;
}(window)(document);

Усовершенствование шаблона enrichment

Стоян Стефанов (Stoyan Stefanov) описывает в своей книге "JavaScript Шаблоны" ("JavaScript Patterns") в гл.6 шаблон "Заимствование конструктора" (к сожалению, в русском издании английские названия шаблонов были опущены и по-этому каноническое название этого шаблона мне пока не удалось найти, в том чиле и на сайте http://www.jspatterns.com/ этот шаблон не упомянут).

Этот шаблон очень напоминает то, что я в одном из предыдущих своих постов назвал "Enrichment", единственное отличие заключается в том, что он предлагает его использование в контексте наследования (в том числе предлагает на его основе реализацию множественного наследования), а не в контексте придания дополнительной функциональности уже сформированным объектам. Читатель, конечно, может считать это одним и тем же шаблоном, однако специфика применения в данном случае, на мой взгляд, имеет важное значение.

Немного попрактиковавшись, я пришёл к выводу о том, что шаблон enrichment луше будет всё-таки описывать именно простой функцией, не пытаясь делать вид, что это конструктор, начиная её не с заглавной буквы, а с приставки "to", что бы не путать её с настящим конструктором. Конструктора же из неё не выйдет просто потому, что, как справедливо указывает Стоян Стефанов в разделе недостатков этого шаблона, она не может прицепить к передаваемому ей объекту ссылку "__proto__" на прототип, а по-этому не может встроить его в модель наследования.

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

function EventW3C() {
    if (!('preventDefault' in this))
        this.preventDefault = function() {
            this.returnValue = false;
        };
    if (!('stopPropagation' in this))
        this.stopPropagation = function(){
            this.cancelBubble = true;
        };
    //Прочие реализации стандартных свойств и методов
    return this;
}
, теперь я пишу так, что, с моей точки зрения, более корректно:
function toW3CEvent(that) {
    for (var p in toW3CEvent)
        if (!(p in that) && toW3CEvent.hasOwnProperty(p) && p !== 'prototype')
            that[p] = toW3CEvent[p];
    return that;
}
toW3CEvent.preventDefault = function() { this.returnValue = false; };
toW3CEvent.stopPropagation = function() { this.cancelBubble = true; };
//Прочие реализации стандартных свойств и методов
Так, теперь для придания объекту event в IE6-8 функциональности стандартного W3C DOM объекта Event, можно вызвать эту функцию следующим образом:
handler.call(that, toW3CEvent(event));

четверг, 10 ноября 2011 г.

Коллекции в JavaScript - допиливаем setAddEventListener

Иногда Java-программистам в JavaScript`е не хватает коллекций. Давайте разберёмся - как можно в JavaScript без написания громоздких тяжеловесных решений обойти ситуацию, для которой нам могла бы понадобиться коллекция?

Итак, в Java есть три типа коллекций - List, Set и Map.

Эмулируем List

Строго говоря, как таковая эмуляция List`а в JavaScript нам в общем-то и не нужна - по сути ей и является массив (Array). Собственно, List`ы и в Java-то нам были нужны лишь исключительно по той причине, что массивы в Java имеют чёткий заранее определённый размер, который не может быть изменён, а по задаче мы не всегда можем сказать, массив какого размера нам понадобится. Т.к. в JavaScript такой проблемы нет - нет и необходимости создавать этот тип коллекций специально.

Эмулируем Set

Множество (Set) уникальных значений в JavaScript можно составить на основании того же массива (Array) с добавлением метода, который проверял бы уникальность добавляемого значения:

/** Определяет индекс элемента в массиве.
 * @param {Object} value
 * @return {number} индекс переданного объекта, если тот содержится в массиве.
 * Если он в нём отсутствует, возарвщается -1. */
Array.prototype.indexOf = function(value) 
{
 var /** @type {number} */ length = this.length;
 for (var /** @type {number} */ i = 0; i < length; i++)
  if (this[i] == value)
   return i;
 return -1;
};
/** Определяет, содержит лимассив переданный объект.
 * @param {Object} value
 * @return {boolean} */
Array.prototype.contains = function(value) 
{
 return this.indexOf(value) === -1;
};
Теперь достаточно просто перед добавлением элемента проверять его методом contains - для простых зазачь этого вполне хватит.

Эмуляция Map

С коллекциями типа map будет немного сложнее. В принципе, для наиболее частого случая, когда в качестве ключей нас устроят строки, решение тривиально - это Object. Т.е. создаём объект - он и есть наша карта. Его свойства - это ключи, а их значения - это значения полей коллекции.

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

Пример эмуляции Map

Например, в одной из недавних статей, где я рассматривал шаблон "Заплатка", я писал о методе создать на основе механизма навешивания слушателей в IE6-8 (attachEvent) стандартный механизм навешивания слушателей (addEventListener). Решение, напомню, было таким:

function setAddEventListener()
{
    if ('attachEvent' in this && !('addEventListener' in this))
        this.addEventListener = function(eventName, handler, isCapturing) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
            attachEvent("on" + eventName, handler);
        }
}
setAddEventListener.call(document);//Для document`а, например, вызываем так
Однако, если подходить серьёзно, то с этим решением в реальном проекте будет некоторое количество проблем. Первая из них - неполная совместимость IE`шного объекта Event и стандартного. Путь для решения этой проблемы я уже демонстрировал в статье про pattern "Enrichment". Напомню, что там получилось:
/** Конструктор, служащий для придания не совместимым с W3C DOM level 3 объектам
 * Event стандартного поведения.
 * @constructor
 * @extends Event */
function EventW3C() {
    /** Отменяет поведение браузера для данного события по-умолчанию. */
    this.preventDefault = function() {
        this.returnValue = false;
    };
    // Выставление других стандартных свойств

    return this;
}
function setAddEventListener()
{
    if ('attachEvent' in this && !('addEventListener' in this))
        this.addEventListener = function(eventName, handler, isCapturing) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
            attachEvent("on" + eventName, function() {
                handler(EventW3C.call(event) //Здесь используется pattern "Enrichment"
            });
        }
}
Вторая проблема - ссылка this. В обработчик IE6-8 по ней не передаётся элемент, с которым связано событие. Решение простое - запомнить ссылку на элемент в отдельной переменной в замыкании и передать её в качестве ссылки this обработчику при помощи метода call объекта Function:
function setAddEventListener()
{
    if ('attachEvent' in this && !('addEventListener' in this))
        this.addEventListener = function(eventName, handler, isCapturing) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");

                //Запоминаем элемент
                var /** @type Element */ that = this;

                this.attachEvent(
                    'on' + eventName, function() {
                        handler.call(that, EventW3C.call(event));//Правильно вызываем обработчик
                    }
                );
        }
}
При этом приёме, правда, как утверждает Д.Флэнаган, есть проблема с утечкой памяти в ранних версиях IE, но это сейчас out of scope. И, наконец, третья проблема - всплывает, когда мы попытаемся рассмотреть применение механизма удаления обработчика IE6-8 detachEvent для эмуляции стандартного removeEventListener. Дело в том, что т.к. мы используем не сам handler, а функцию, которая его вызывает, то и detach`ить нам нужно её, а не изначальный handler. Соответственно, для удаления нам где-то нужно её найти, так? Но в стандартный метод removeEventListener будет передаваться лишь сам handler. Значит, нам нужно по handler`у найти настраивающую его функцию. Тут-то нам и понадобится коллекция типа Map.

Итак, есть соответствие двух функций и по ключу - одной функции мы должны найти значение - другую функцию, что бы её удалить из обработчиков данного события. Т.к. Map мы эмулируем на основе Object`а, у которого в качестве имён полей, т.е. ключей, традиционно выступают строки, то нам, очевидно, нужен метод, который преобразует объект в уникальную для него строку. В общем случае эта задача не является тривиальной, но в нашем конкретном - какая удача! - для функций, т.е. для объектов Function, этот метод уже есть! И называется он - ни за что не догадаетесь! - toString()! :)))) Этот метод по сути возвращает представление данной функции в виде многострочного текста её определения. Очевидно, что текст определения функции не меняется никогда и он соответствует критерию уникальности для каждой функции, а для одной и той же функции всегда будет возвращать одинаковый результат.

Так что вот какое компактное решение получаем в данном случае:

/** Конструктор, служащий для придания не совместимым с W3C DOM level 3 объектам Event стандартного поведения.
 * @constructor
 * @extends Event */
function EventW3C() {
    /** Отменяет поведение браузера для данного события по-умолчанию. */
    if (!('preventDefault' in this))
        this.preventDefault = function() {
            this.returnValue = false;
        };

    if (!('stopPropagation' in this))
        this.stopPropagation = function(){
            this.cancelBubble = true;
        };

    // Выставление других стандартных свойств

    return this;
}

(/** IE patch for addEventListener and removeEventListener methods. */
function setAddEventListener() {
    if (!('addEventListener' in this))
        if ('attachEvent' in this) {
            var /** Объект-карта всех слушателей данного объекта
                 * @type Object<Object<Function>> */
                fnMap = {};
            this.addEventListener = function(eventName, handler) {
                if (!(eventName in fnMap)) //Если для данного события ещё нет коллекции обработчиков...
                    /**@type Object<Function> */ fnMap[eventName] = {}; //...создаём её
                var /** @type Element */ that = this; //Запоминаем элемент
                this.attachEvent('on' + eventName,
                    fnMap[eventName][handler.toString()] = function() {
                        handler.call(that, EventW3C.call(event));//Правильно вызываем обработчик
                    }
                );
            };
            this.removeEventListener = function(eventName, handler) {
                if (eventName in fnMap && handler.toString() in fnMap[eventName]) {
                    this.detachEvent(
                        'on' + eventName,
                        fnMap[eventName][handler.toString()]
                    );
                    delete fnMap[eventName][handler.toString()];
                }
            };
        }
})(); // Вызываем для window сразу
setAddEventListener.call(document);//Для document`а, например, вызываем так

пятница, 7 октября 2011 г.

Самонастраивающаяся функция

В ходе написания второй версии библиотеки для Drag&Drop`а (уже скоро выложу и опубликую, ждите - осталось недолго :) ) была найдена парочка интересных JavaScript`овых решений, которые тоже вполне могут претендовать на звание Pattern`ов. Рискуя тем, что, возможно, изобретаю велосипед, всё-таки распишу, как я их использую.

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

Конечно, можно сделать в этой функции большой оператор ветвления if с тем, что бы в случае одних параметров выполнять одну логику, а в случае других - другую. Но эта операция проверки будет отнимать дополнительное время, так что это приемлемо лишь для не очень критичной к скорости работы логики.

Именно с такой задачей я имел дело при написании второй версии своей библиотечки. Мне нужна была функция, которой передавался бы объект pos, имеющий два свойства - 'x' и 'y' и от неё требовалось, что бы она проставила в них значения x и y-координат курсора мыши. Я назвал её refreshMousePos.

Саму эту функцию должен вызывать обработчик того или иного события, который в W3C-совместимых браузерах получает ссылку на объект Event, соответствующий данному событию. Для вычисления координат курсора мыши этот объект нужен, так что в этом случае ссылку на него нужно передать в refreshMousePos вторым аргументом. В случае же W3C-несовместимого браузера (в основном, IE6-8), обработчику не передаётся этот объект, по-этому он и не может быть передан в функцию refreshMousePos.

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

Но заранее определить точно, будет или не будет передан объект Event в функцию, затруднительно - гораздо удобнее это сделать в процессе работы, по факту проверив содержимое переданной переменной. При чём, это достаточно сделать один раз и сразу будет понятно, как функция должна выполняться впоследствии.

Сам IE меняет в соответствии с обрабатываемым событием объект event, который всегда находится у него в глобальном контексте. И интерфейс этого объекта несколько иной, что проявляется в логике вычисления координат курсора мыши.

Так что я сделал разделение логики следующим образом:

/** Обновить координаты мыши.
 * @param {Position} pos координаты курсора мыши
 * @param {Event} [evt] объект события */
function refreshMousePos(pos, evt) {
    return (refreshMousePos = typeof evt !== 'undefined' ?
        function(pos, evt) { //W3C realization
            var body = document.body;
            pos.x = evt.clientX + body.scrollLeft - body.clientLeft;
            pos.y = evt.clientY + body.scrollTop - body.clientTop;
            return pos;
        } :
        function(pos) { //IE realization
            pos.x = event.pageX;
            pos.y = event.pageY;
            return pos;
        }
    )(pos, evt); //вызываем тот вариант функции, которую присвоили и возвращаем результат выполнения
};
Как видим, здесь переменной, которая содержит главную функцию, в зависимости от содержания ссылки evt присваивается различное значение-функция, при этом выполняющаяся в данный момент функция автоматически затирается. Т.е. функция, будучи вызванной, как бы более тонко настраивает себя под конкретную среду в которой оказалась для более производительной работы в ней.

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

alert(
    refreshMousePos(pos, evt).x
);

пятница, 16 сентября 2011 г.

"Заплатка" ("Patch") как JavaScript шаблон проектирования

Недавно пришла в голову мысль, что "Заплатка" вполне заслуживает звания JavaScript-шаблона проектирования. По-англицки можно было бы назвать "Patch". Суть его не собственно в хаке, а скорее в том, что бы отделять код, написанный в соответствии со стандартом, от кода, который приспосабливает браузер, не поддерживающий стандарты, корректно работать со стандартным кодом при помощи того или иного хака.

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

Итак, часто JavaScript-разработчики вставляют код, направленный на совместимость, непосредственно внутрь логики основного сценария. Например, если нам нужно установить слушателя события загрузки страницы, мы могли бы написать:

function onLoadListener() {
    //some code...
}
if ('addEventListener' in window)
    addEventListener("load", onLoadListener, false);
else if ('attachEvent' in window)
    attachEvent("onload", onLoadListener)
В принципе, ничего нет плохого в том, что бы так делать, однако проблема заключается в том, что при написании сложных сценариев ваша голова итак забита сложностью решаемой задачи и Вам не захочется отвлекаться ещё и на все эти глупые браузеросовместимости - вам бы хоть как-то её решить. И потом, при отладке и чтении кода вам всё время будет попадаться на глаза этот фрагмент и мозолить глаза. С точки зрения алгоритма он не несёт никакого смысла, а на экране занимает место, которое могли бы занять более полезные смысловые фрагменты кода - видя их одновременно, без прокрутки, Вам могла бы придти в голову ценная мысль, которая не пришла бы, если бы вам пришлось вращать колесо мыши, что бы всё увидеть.

Тем не менее, сократить эту конструкцию до стандартного кода

addEventListener("load", function() {
    //some code...
}, false);
мы не можем, поскольку стандарты поддерживают не все браузеры, в которых нам хотелось бы, что бы наш сценарий успешно выполнялся. Для остальных же браузеров у нас есть многочисленные хаки. Так как же быть?

Как только я в своих проектах сталкиваюсь с несовместимостью работы того или иного браузера со стандартными методами, первая мысль у меня возникает о том, что бы вынести обеспечение совместимости в отдельный от основного код, что бы он его не захламлял. У меня уже накопилось достаточно много таких заплаток, латающих различные дыры в поддержке стандартов различным браузерами - главным образом IE. Обычно я помещаю их в отдельный файл проекта, называя его "commons.js" (есть и исключения - например в случае, если это нужно только для тестирования, я считаю не зазорным вставлять такой код просто в начало тестового файла).

В данном случае полезной была бы следующая заплатка:

(function setAddEventListener()
{
    if ('attachEvent' in this && !('addEventListener' in this))
        addEventListener = function(eventName, handlerб isCapturing) {
            if (isCapturing)
                throw new Error("We are in IE, so we haven`t way to set event listener on capturing phase of any event to any of HTML-elements");
            attachEvent("on" + eventName, handler);
        }
})(); //IE patch for window.addEventListener
Здесь элегантно используется то обстоятельство, что ссылка this при вызове в глобальном контексте в функции указывает на объект window. По-этому сразу же после объявления функция просто вызывается.

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

Если необходима реализация парного метода removeEventListener, её легко написать по аналогии, реализовав через метод detachEvent.

Теперь другие элементы, которым так же нужно "привить" правильное поведение, могли бы обрабатываться следующим образом:

setAddEventListener.call(document.getElementById('id1'));
В статье "JavaScript: Реализация pattern`а Singleton" я уже приводил ещё одну заплатку - для безпроблемного объявления объекта XMLHttpRequest (который я нашёл в англоязычной Wikipedia):
// Provide the XMLHttpRequest class for IE 5.x-6.x:
if (typeof XMLHttpRequest == "undefined")
    /** @constructor */
    XMLHttpRequest = function() {
        try { return new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch(e) {}
        try { return new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch(e) {}
        try { return new ActiveXObject("Msxml2.XMLHTTP") } catch(e) {}
        try { return new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}
        throw new TypeError( "This browser does not support XMLHttpRequest." )
    };
Напоследок приведу ещё одну заплатку. Она служит для безпроблемной реализации стандартного метода window.getComputedStyle, отсутствующего в IE вплоть до 8 версии включительно. Кто не знает, это очень удобный метод, позволяющий получать ссылки на объект, содержащий информацию о "вычислимых стилях" для HTML-элемента. Вычислимым называется стиль, специально не установленный разработчиком, однако получившийся в результате вывода браузером этого элемента на странице. IE вместо него имеет свойство computedStyle, доступное у каждого элемента. К сожалению, оно не может представить вычислимый стиль для псевдоклассов, по-этому заплатку можно вставить так же, как и для addActionListener`а - лишь частичную.

Таким образом, вот как можно решить данную проблему несовместимости IE со стандартом:

//IE patch for window.getComputedStyle
if (!('getComputedStyle' in window))
    getComputedStyle = function(element, pseudoclass) {
        if (pseudoclass === null || typeof pseudoclass === 'undefined')
            return element.currentStyle;
        else
            throw new Error("We are in IE, so we haven`t way to get pseudoclass styles of element");
    };

воскресенье, 19 апреля 2009 г.

JavaScript: небольшое отличие function от function

Некоторое время назад я писал на форуме, что не вижу разницы между следующими конструкциями:
function fnName() {
//...
}
, и
var fnName = function () {...}
, но на самом деле разница есть. Состоит она в том, что в первом случае функция определяется на этапе синтаксического анализа, а во втором - в момент выполнения. Что бы её продемонстрировать, давайте посмотрим на следующий пример:
x(); // Выводит "1", хотя функция ещё вроде бы не определена...
var x = 2;
alert(typeof x); // Выводит "number"
// Следующая строка выполнилась ещё до того, как выполнилась первая строка данного
// примера, так что сейчас она ничего не меняет.
function x(){alert(1);}
alert(typeof x); // Опять выводит "number"
var x = function(){alert(3);}
x(); // Выводит "3". Функция переопределена.