7
\$\begingroup\$

I'm creating an event hub object in for a large project I am building in JavaScript. The project will have modules in charge of the UI, commands, and rendering. Each module will use this hub for broadcasting events to each other.

This is my JavaScript library (I cut out RequireJS for conciseness):

Hub = Object.create ({});
// initialize this hub
Hub.init = function () {
 this.events = {};
 this.contexts = {};
 return this;
};
// register a function and context
Hub.on = function (name, callback, contexts) {
 // create event type if it isn't already and push our callback
 (this.events [name] || (this.events [name] = [])).push (callback);
 (this.contexts [name] || (this.contexts [name] = [])).push (contexts);
};
// un-register a function
Hub.off = function (name, callback) {
 // if this event type exists, splice out our callback
 this.events [name] && this.events [name].splice (this.events [name].indexOf (callback), 1);
 this.contexts [name] && this.contexts [name].splice (this.events [name].indexOf (callback), 1);
};
// fire all of a type of functions
Hub.trigger = function (name) {
 if (!this.events [name] || this.events [name].length === 0) return;
 var args = Array.prototype.slice.call (arguments, 1),
 i = 0, event = this.events [name], context = this.contexts [name], l = event.length;
 // if this event type exists, run all the callbacks
 for (; i < l; event [i].apply (context [i++], args));
};

And this about how it is used:

// MAIN.JS: create the main hub
Main.hub = Object.create (Hub);
// RENDER.JS: listen for entity addition
Main.hub.on ('entity_add', function (entity) {
 this.draw (entity);
}, this);
// DRAW_CMD.JS: listen for canvas click
DrawCMD.init = function () {
 Main.hub.on ('canvas_click', this.clicked, this);
};
DrawCMD.clicked = function (ev) {
 // tell everyone that an entity is added
 Main.hub.trigger ('entity_add', this.entity);
 Main.hub.off ('canvas_click', clicked);
};
// UI.JS: fire canvas click
canvas.addEventListener ('click', function (ev) {
 Main.hub.trigger ('canvas_click', ev);
});

So, I have a few questions about this:

  1. Efficiency: There are a few scenarios where this seems resource-wasteful, for example: when there are no commands active, click and mouse-move events are still being broadcast. Are there ways I make my .trigger function faster for this?
  2. Organization: Does this seem like it will make my project more or less elegant compared to just directly calling functions on each relevant module?
  3. OOP: This is my first attempt at using the new Object.create. Could I be using it better, or does this look good?
  4. Context: I am using two arrays (events and contexts) so I can have registered functions keep the same context they would have normally. I would use Function.bind, except that doesn't allow me to use .off to un-register events. Is there a more elegant way to do this?
  5. Messes: Does anyone see any other potential pitfalls with this approach?
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jan 30, 2015 at 18:00
\$\endgroup\$
1
  • \$\begingroup\$ Backbone has this built-in so I'm using that now: BackboneJS#Events \$\endgroup\$ Commented Aug 7, 2015 at 20:24

1 Answer 1

3
\$\begingroup\$

It looks good to me, but your questions are hard to evaluate with such a small snippet.

  1. Efficiency

    Basically, what else can you do? You have to iterate over all registered events.

  2. Organisation

    It seems like a good idea to me. This way you would have central point for binding events to DOM elements - if I'm understanding your code correctly.

  3. OOP

    Go for it!

  4. Context

    This is problematic, but not dramatic! Basically, your .off function does not keep the to arrays in sync, which is really hard because events are asynchronous.

    Consider this example with simple inheritance:

    <html>
    <body>
    <div id="clickMe">Click Me!</div>
    <script>
    (function(){
     var Hub = Object.create ({});
     //initialize this hub
     Hub.init = function () {
     this.events = {};
     this.contexts = {};
     return this;
     };
     //register a function and context
     Hub.on = function (name, callback, contexts) {
     // create event type if it isn't already and push our callback
     (this.events [name] || (this.events [name] = [])).push (callback);
     (this.contexts [name] || (this.contexts [name] = [])).push (contexts);
     };
     //un-register a function
     Hub.off = function (name, callback) {
     // if this event type exists, splice out our callback
     console.log(this.events [name].indexOf (callback));
     console.log((this.events));
     console.log(JSON.stringify(this.contexts));
     this.events [name] && this.events [name].splice (this.events [name].indexOf (callback), 1);
     this.contexts [name] && this.contexts [name].splice (this.events [name].indexOf (callback), 1);
     console.log((this.events));
     console.log(JSON.stringify(this.contexts));
     };
     //fire all of a type of functions
     Hub.trigger = function (name) {
     if (!this.events [name] || this.events [name].length === 0) return;
     var args = Array.prototype.slice.call (arguments, 1),
     i = 0, event = this.events [name], context = this.contexts [name], l = event.length;
     // if this event type exists, run all the callbacks
     for (; i < l; event [i].apply (context [i++], args));
     };
     var Main = {};
     Main.hub = Object.create(Hub);
     Main.hub.init();
     var Foo = {};
     Foo.init = function (name) {
     this.name = name;
     Main.hub.on("lala", this.onClick, this);
     };
     Foo.onClick = function Foo_onClick (){
     alert(this.name);
     };
     var Bar = Object.create(Foo);;
     Bar.init = function (name) {
     Foo.init.call(this, name);
     };
     Bar.onClick = function Bar_onClick (){
     Foo.onClick.call(this);
     Main.hub.off("lala", this.onClick);
     };
     var f = Object.create(Foo);
     var b = Object.create(Bar);
     var c = Object.create(Foo);
     f.init("foo");
     b.init("bar");
     c.init("c");
     console.log(Object.is(f,c));
     var clickMe = document.getElementById("clickMe");
     clickMe.addEventListener("click",function(e){
     Main.hub.trigger("lala",e);
     });
    }());
    </script>
    </body>
    </html>
    

    Here, calling Hub.off inside an event callback forces Hub.events.length to decrement while i in Hub.trigger is still incrementing, so event[i] will be undefined. Also, this.events[name].indexOf(callback) will give a different result in the second line:

    this.events [name] && this.events [name].splice (this.events [name].indexOf (callback), 1);
    this.contexts [name] && this.contexts [name].splice (this.events [name].indexOf (callback), 1);
    
  5. Messes

    Besides (4), I'd recommend another formatting style. Yours is really hard to read. This is important, if you consider your product to be maintained in the long run.

  6. Solution for asynchronous calls for .on and .off

    Here is a solution for a Hub, which can deal with asynchrounous calls of .on and .off. I also made some minor API changes, which strictly (un-)registers only pairs of (callback, context). I'm not certain if this is necessary for unregister, but it makes the API more symmetric.

    var Hub = Object.create ({});
    // initializes Hub
    Hub.init = function () {
     this._handlers = {}; // centra registry for custom events
     this._running = false; // determines if custom evetns ar triggered
    };
    // delays the exectuion of fn while Hub is triggering custom events (_running === true)
    Hub._delay = function Hub_delay (fn) {
     var hub, interval, id;
     hub = this;
     interval = 0;
     // setInterval(fn,0) is the JS equivalent for while(true){}
     // the actual while(true) will certainly kill the process
     id = setInterval(function(){
     if (!this._running) {
     fn.call(hub);
     clearInterval(id);
     }
     },interval);
    };
    // registers the pair (callback, context) for the custom event name
    Hub.on = function Hub_on (name, callback, context) {
     this._delay(function Hub_on_delayed (){
     var handler;
     if (!Array.isArray(this._handlers[name])) {
     this._handlers[name] = [];
     }
     handler = {};
     handler.callback = callback;
     handler.context = context;
     this._handlers[name].push(handler);
     });
    };
    // unregisters the pair (callback, context) for the custom event name
    Hub.off = function Hub_off (name, callback, context) {
     this._delay(function Hub_off_delayed (){
     if (!Array.isArray(this._handlers[name])) {
     this._handlers[name] = [];
     }
     console.log(JSON.stringify(this._handlers[name]));
     this._handlers[name] = this._handlers[name].filter(function(handler){
     return !(handler.callback === callback && handler.context === context);
     });
     console.log(JSON.stringify(this._handlers[name]));
     });
    };
    // triggers all handlers for the custom event name
    Hub.trigger = function Hub_trigger (name) {
     var args, i, handlers, callback, context, invoke;
     // delay asynchronous registering and unregistering
     this._running = true;
     args = Array.prototype.slice.call (arguments, 1);
     handlers = Array.isArray(this._handlers[name]) ? this._handlers[name] : [];
     for (i = 0; i < handlers.length; i++) {
     callback = handlers[i].callback;
     context = handlers[i].context; 
     // allow invokation only fo valid callbacks and contexts
     invoke = (
     typeof callback === "function" 
     && typeof context !== "undefined"
     && context !== null
     );
     if (invoke === true) {
     callback.apply(context, args);
     }
     }
     // allow registering and unregistering
     this._running = false;
    };
    
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
answered Jul 18, 2015 at 22:54
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.