Программно реализуя обратные вызовы с помощью JS/jQuery
Итак, я пишу веб-приложение. Практически все делается на стороне клиента, сервер - это только интерфейс RESTful. Я использую jQuery в качестве моей выборки и реализую свой код в Выявление шаблона модуля.
Каркас моего кода в основном выглядит следующим образом:
(function($){
$.fn.myplugin = function(method)
{
if (mp[method])
{
return mp[method].apply(this, Array.prototype.slice.call(arguments, 1));
}
else if (typeof method === 'object' || ! method)
{
return mp.init.apply(this, arguments);
}
else
{
$.error('Method ' + method + ' does not exist on $.myplugin');
}
};
var mp =
{
init : function( options )
{
return this.each(function()
{
// stuff
}
},
callbacks : {},
addCallback : function(hook_name, cb_func, priority)
{
// some sanity checking, then push cb_func onto a stack in mp.callbacks[hook_name]
},
doCallbacks : function(hook_name)
{
if (!hook_name) { hook_name = arguments.callee.caller.name; }
// check if any callbacks have been registered for hook_name, if so, execute one after the other
}
};
})(jQuery);
Довольно просто, правильно?
Теперь мы можем регистрировать (множественные, иерархические) обратные вызовы изнутри, а также извне области приложения.
Что меня подслушивает: чтобы сделать все возможное настолько расширяемым, мне пришлось бы прибегнуть к чему-то в этом направлении:
foo : function() {
mp.doCallbacks('foo_before');
// do actual stuff, maybe some hookpoints in between
mp.doCallbacks('foo_after');
}
Каждая отдельная функция внутри моего приложения должна начинаться и заканчиваться так. Это просто не кажется правильным.
Итак, JS волшебники SO - что делать?
Ответы
Ответ 1
Вы можете написать функцию, которая принимает другую функцию в качестве аргумента, и возвращает новую функцию, которая вызывает ваши крючки вокруг этого аргумента. Например:
function withCallbacks(name, func)
{
return function() {
mp.doCallbacks(name + "_before");
func();
mp.doCallbacks(name + "_after");
};
}
Затем вы можете написать что-то вроде:
foo: withCallbacks("foo", function() {
// Do actual stuff, maybe some hookpoints in between.
})
Ответ 2
Возможно, я не понял вопрос правильно, потому что не понимаю, почему вы не добавляете код для вызова обратных вызовов непосредственно в код myplugin:
$.fn.myplugin = function(method)
{
if (mp[method])
{
var params = Array.prototype.slice.call(arguments, 1), ret;
// you might want the callbacks to receive all the parameters
mp['doCallbacks'].apply(this, method + '_before', params);
ret = mp[method].apply(this, params);
mp['doCallbacks'].apply(this, method + '_after', params);
return ret;
}
// ...
}
EDIT:
Хорошо, прочитав ваш комментарий, я думаю, что другим решением будет (конечно) другое направление. То есть, вызывается функция invoke, которая используется от конструктора, а также другие общедоступные методы для вызовов между собой. Я хотел бы отметить, что он будет работать только для общедоступных методов, так как привязка к закрытым методам в любом случае прерывает инкапсуляцию.
Простая версия будет примерно такой:
function invoke(method) {
var params = Array.prototype.slice.call(arguments, 1), ret;
// you might want the callbacks to receive all the parameters
mp['doCallbacks'].apply(this, method + '_before', params);
ret = mp[method].apply(this, params);
mp['doCallbacks'].apply(this, method + '_after', params);
}
$.fn.myplugin = function() {
// ...
invoke('init');
// ...
};
Но я на самом деле написал немного больше кода, что уменьшит дублирование между плагинами.
Таким образом, создание плагина будет выглядеть в конце
(function() {
function getResource() {
return {lang: "JS"};
}
var mp = NS.plugin.interface({
foo: function() {
getResource(); // calls "private" method
},
bar: function() {
this.invoke('foo'); // calls "fellow" method
},
init: function() {
// construct
}
});
$.fn.myplugin = NS.plugin.create(mp);
})();
И вот как выглядит частичная реализация:
NS = {};
NS.plugin = {};
NS.plugin.create = function(ctx) {
return function(method) {
if (typeof method == "string") {
arguments = Array.prototype.slice.call(arguments, 1);
} else {
method = 'init'; // also gives hooks for init
}
return ctx.invoke.apply(ctx, method, arguments);
};
};
// interface is a reserved keyword in strict, but it descriptive for the use case
NS.plugin.interface = function(o) {
return merge({
invoke: NS.plugin.invoke,
callbacks: {},
addCallback: function(hook_name, fn, priority) {},
doCallbacks: function() {}
}, o);
};
NS.plugin.invoke = function(method_name) {
if (method_name == 'invoke') {
return;
}
// bonus (if this helps you somehow)
if (! this[method]) {
if (! this['method_missing') {
throw "Method " + method + " does not exist.";
} else {
method = 'method_missing';
}
}
arguments = Array.prototype.slice.call(arguments, 1);
if (method_name in ["addCallbacks", "doCallbacks"]) {
return this[method_name].apply(this, arguments);
}
this.doCallbacks.apply(this, method_name + '_before', arguments);
var ret = this[method_name].apply(this, arguments);
this.doCallbacks.apply(this, method_name + '_after', arguments);
return ret;
};
Конечно, это полностью непроверено:)
Ответ 3
вы по существу используете усеченную версию jQueryUI Widget factory. я бы рекомендовал использовать эту функциональность, чтобы избежать необходимости откатывать это самостоятельно.
виджет factory будет автоматически магически отображать строки в вызовы методов, такие как:
$("#foo").myPlugin("myMethod", "someParam")
вызовет myMethod
в экземпляре плагина с 'someParam'
в качестве аргумента. Кроме того, если вы запускаете пользовательское событие, пользователи могут добавлять обратные вызовы, добавляя свойство к параметрам, которые соответствуют имени события.
Например, виджет tabs имеет событие select
, к которому вы можете подключиться, добавив свойство select
к во время инициализации:
$("#container").tabs({
select: function() {
// This gets called when the `select` event fires
}
});
конечно же, вам нужно будет добавить до и после крючков в качестве событий, чтобы иметь возможность использовать эту функцию, но это часто приводит к упрощению обслуживания.
надеюсь, что это поможет. ура!
Ответ 4
В основном я предпочитаю избегать обратных вызовов и использовать события. Причина проста. Я могу добавить несколько функций для прослушивания данного события, мне не нужно возиться с параметрами обратного вызова, и мне не нужно проверять, определен ли обратный вызов. Поскольку все ваши методы вызывают через $.fn.myplugin
, легко запускать события до и после вызова метода.
Вот пример кода:
(function($){
$.fn.myplugin = function(method)
{
if (mp[method])
{
$(this).trigger("before_"+method);
var res = mp[method].apply(this, Array.prototype.slice.call(arguments, 1));
$(this).trigger("after_"+method);
return res;
}
else if (typeof method === 'object' || ! method)
{
return mp.init.apply(this, arguments);
}
else
{
$.error('Method ' + method + ' does not exist on $.myplugin');
}
};
var mp =
{
init : function( options )
{
$(this).bind(options.bind);
return this.each(function()
{
// stuff
});
},
foo: function() {
console.log("foo called");
}
};
})(jQuery);
$("#foo").myplugin({
bind: {
before_foo: function() {
console.log("before foo");
},
after_foo: function() {
console.log("after foo");
}
}
});
$("#foo").myplugin("foo");