开发者

Programmatically implementing callbacks with JS/jQuery

开发者 https://www.devze.com 2023-04-09 10:24 出处:网络
So, I\'m writing a web app. Pretty much everything is done client-side, the server is but a RESTful interface. I\'m using jQuery as my framework of choice and implementing my code in a Revealing Modul

So, I'm writing a web app. Pretty much everything is done client-side, the server is but a RESTful interface. I'm using jQuery as my framework of choice and implementing my code in a Revealing Module Pattern.

The wireframe of my code basically looks like this:

(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
            }
        },
        call开发者_开发技巧backs : {},
        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);

Pretty straightforward, right?

Now, we're able to register (multiple, hierarchical) callbacks from inside as well as from outside the application scope.

What is bugging me: To make the whole thing as extensible as possible, I'd have to resort to something along these lines:

foo : function() {
    mp.doCallbacks('foo_before');
    // do actual stuff, maybe some hookpoints in between
    mp.doCallbacks('foo_after');        
}

Every single function inside my app would have to start and end like that. This just doesn't seem right.

So, JS wizards of SO - what do?


You can write a function that takes another function as an argument, and returns a new function that calls your hooks around that argument. For instance:

function withCallbacks(name, func)
{
    return function() {
        mp.doCallbacks(name + "_before");
        func();
        mp.doCallbacks(name + "_after"); 
    };
}

Then you can write something like:

foo: withCallbacks("foo", function() {
    // Do actual stuff, maybe some hookpoints in between.
})


I might have not understood the question correctly, because I don't see why you don't add the code to call the callbacks directly in the myplugin code:

$.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:

Ok, after reading your comment I think another solution would be (of course) another indirection. That is, have an invoke function that's being used from the constructor as well as the other public methods for calls between themselves. I would like to point out that it will only work for the public methods, as attaching to private methods' hooks breaks encapsulation anyway.

The simple version would be something like this:

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');
   // ...
};

But, I've actually written a bit more code, that would reduce the duplication between plugins as well. This is how creating a plugin would look in the end

(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);

})();

And this is how the partial implementation looks like:

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's 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;
};

Of course, this is completely untested :)


you are essentially implementing a stripped-down version of the jQueryUI Widget factory. i'd recommend using that functionality to avoid having to roll this yourself.

the widget factory will auto-magically map strings to method calls, such that:

$("#foo").myPlugin("myMethod", "someParam")

will call myMethod on the plugin instance with 'someParam' as an argument. Additionally, if you fire a custom event, users can add callbacks by adding an property to the options that matches the event name.

For example, the tabs widget has a select event that you can tap into by adding a select property to the options during initialization:

$("#container").tabs({
    select: function() {
        // This gets called when the `select` event fires
    }
});

of course, you'll need to add the before and after hooks as events to be able to borrow this functionality, but that often leads to easier maintenance anyhow.

hope that helps. cheers!


Basically I prefer to avoid callbacks and use events instead. The reason is simle. I can add more than one functions to listen given event, I don't have to mess with callback parameters and I don't have to check if a callback is defined. As far as all of your methods are called via $.fn.myplugin it's easy to trigger events before and after method call.

Here is an example code:

(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");
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号