пятница, 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
);

2 комментария:

Восьменог комментирует...

Может я чего-то не понял, но в твоём решении условие проверяется каждый раз, поскольку выполняемая функция "не затирается".
Посмотри код:

[code]
console.clear();

var q=1

function t(a){
console.log('invoked');
return a
}

function r(pos, evt) {
return (t(q) == 1
? function(pos, evt) {
return pos;
}
: function(pos) {
return pos;
}
)(pos, evt);
};


console.log(r(1));
console.log(r(2));
console.log(r(3));
[/code]

Одно из решений - каррирование функции "на месте".
[code]
var r = (function(){
return (t(q)==1 ?
function(pos, evt) {
return pos;
} :
function(pos) {
return pos;
}
);
})()

console.log(r(1));
console.log(r(2));
console.log(r(3));
[/code]

Самое простое решение:
[code]
var r = (t(q) == 1)
? function(pos, evt) {
return pos;
} :
function(pos) {

return pos;
}

console.log(r(1));
console.log(r(2));
console.log(r(3));
[/code]

C'est la vie комментирует...

Восьменог, так ты в этом коде, действительно, не затираешь функцию. Ведь, затирка происходит по её имени, при присвоении переменной другого значения. В данном случае вот как твой первый код нужно изменить, что бы затирка произошла (нужное место я выделил жирным):
var q=1;

function t(a)
{
    console.log('invoked');
    return a;
};

function r(pos, evt)
{
    return (r = t(q) == 1 ?
        function(pos, evt) {
            return pos;
        } :
        function(pos) {
            return pos;
        }
    )(pos, evt);
};

console.log(r(1));
console.log(r(2));
console.log(r(3));

Что же касается решения во втором участке кода, то оно хорошее, но не подъодит для джанного случая - я же и пишу, что не всегда мы сначала можем правильно понять, что нам нужно - моё решение как раз для случая, когда мы можем понять, какая функция нужна, лишь на этапе первого её вызова. :)