JavaScript EventManager 4 U

Started by
7 comments, last by BUnzaga 14 years, 6 months ago
I have been working on this for a bit now, I am a fan of collaboration, so I figured I would release it out into the universe and see if anyone could use it, or make it better, etc. The engine I use (DX Studio) uses &#106avascript as its scripting language, so this is basically a &#106avascript EventManager. It also incorporates a singleton approach, so there is &#111;nly ever &#111;ne EventManager in memory at any given time. My hopes is that people can use this if they need an event manager in &#106avascript, or perhaps see how I did this, and make their own Event Manager in their code of choice. Here is the actual code: <!--STARTSCRIPT--><!--source lang="cpp"--><div class="source"><pre> <span class="cpp-comment">//special thanks to DX Studio forum user Pat for helping me better</span> <span class="cpp-comment">//understand these concepts and posting his WIP event system.</span> <span class="cpp-comment">/* v10112009 BUnzaga */</span> EventManager._em = null; function EventManager(){ <span class="cpp-comment">//if we haven't already created an EventManager, create one</span> <span class="cpp-keyword">if</span>(EventManager._em == null){ EventManager._em = <span class="cpp-keyword">this</span>; } <span class="cpp-comment">//if we already have an EventManager, return that event manager.</span> <span class="cpp-keyword">else</span>{ <span class="cpp-keyword">return</span> EventManager._em; } <span class="cpp-comment">//map of event types</span> var listeners = {}; <span class="cpp-comment">//helper function for Array object, more accurate than</span> function isNull(element, index, array){ <span class="cpp-keyword">return</span> element == null; } <span class="cpp-comment">/* Registers a listener for an Object. Pass in the Object, a string name for the event to listen for and the callback function. */</span> <span class="cpp-keyword">this</span>.registerListener = function(obj, e, callback){ <span class="cpp-keyword">if</span>(!(e in listeners)){ <span class="cpp-comment">//create a new array of objects that are listening for e.</span> listeners[e] = []; } <span class="cpp-comment">//see if we are already listening for this object/event/callback</span> var j=listeners[e].length; var i = <span class="cpp-number">0</span>; <span class="cpp-keyword">for</span>(; i &lt; j; i++){ <span class="cpp-keyword">if</span>(listeners[e]<span style="font-weight:bold;">['obj'] === obj &amp;&amp; listeners[e]<span style="font-weight:bold;">['callback'] === callback){ <span class="cpp-keyword">return</span>; } } <span class="cpp-comment">//add the object/callback to the event</span> listeners[e].push({obj:obj, callback:callback}); } <span class="cpp-comment">/* Unregisters a listener for an Object. Pass in the Object and the event string name it was listening for. If there are no more Objects tied to the event, the event listener will be deleted. You can also optionally pass in the callback function, if there is more than one callback listening for this event. */</span> <span class="cpp-keyword">this</span>.unregisterListener = function(obj, e, callback){ <span class="cpp-keyword">if</span>(e in listeners){ <span class="cpp-comment">//if there was no callback function passed in</span> <span class="cpp-keyword">if</span>(callback == null){ <span class="cpp-keyword">for</span>(var i = listeners[e].length-<span class="cpp-number">1</span>; i &gt; -<span class="cpp-number">1</span>; i--){ <span class="cpp-keyword">if</span>(listeners[e]<span style="font-weight:bold;">['obj'] === obj){ listeners[e].splice(i, <span class="cpp-number">1</span>); } } } <span class="cpp-keyword">else</span>{ <span class="cpp-comment">//if there was a callback function passed in</span> <span class="cpp-keyword">for</span>(var i = listeners[e].length-<span class="cpp-number">1</span>; i &gt; -<span class="cpp-number">1</span>; i--){ <span class="cpp-keyword">if</span>(listeners[e]<span style="font-weight:bold;">['obj'] === obj &amp;&amp; listeners[e]<span style="font-weight:bold;">['callback'] === callback){ listeners[e]<span style="font-weight:bold;"> = null; listeners[e].splice(i, <span class="cpp-number">1</span>); } } } <span class="cpp-comment">//if there are no more objects listening for this event type</span> <span class="cpp-comment">//delete the event from the listeners map</span> <span class="cpp-keyword">if</span>(listeners[e].every(isNull)){ listeners[e] = null; <span class="cpp-keyword">delete</span> listeners[e]; } } } <span class="cpp-comment">/* Raises an event. Pass in the event string, and an optional 'data' object this could be an object, an array, a value, just about anything. */</span> <span class="cpp-keyword">this</span>.raiseEvent = function(e, data){ <span class="cpp-comment">//if we are listening for this event</span> <span class="cpp-keyword">if</span>(e in listeners){ <span class="cpp-keyword">for</span>(var i = (listeners[e].length-<span class="cpp-number">1</span>);i &gt; -<span class="cpp-number">1</span>; i--){ <span class="cpp-comment">//call all the callback functions listening for the event, passing the data</span> <span class="cpp-comment">//to be used in the callback function</span> listeners[e]<span style="font-weight:bold;">['callback'].call(listeners[e]<span style="font-weight:bold;">['obj'], data); } } } } </pre></div><!--ENDSCRIPT--> Here is an example of it in a web html page: <a href = "http://www.samotigames.com/tests/BUnzaga_EM_Test.html" target = "new">Samoti Games</a> http://www.samotigames.com/tests/BUnzaga_EM_Test.html I don't claim to be a &#106avascript guru, so I hope anyone who knows how to make this faster or better would post their findings. A couple caveats: I made the option to pass in the callback function, for a couple reasons. First, sometimes the same object might want to listen for the same event type, but for different callbacks. Such an example, perhaps you want to listen for moving the mouse &#111;n the X axis and separately listen for the Y axis. This way you can register to listen for mouseMove independent of each other. Secondly, sometimes the event doesn't explicitly match the callback function name. I ran into this when I was loading files, and raising an event with the filename as part of the event, but the callback function was the same for every file loader object. IE: fileName+"_loded" would be the event, and 'onLoadComplete' was the function name. Overall I like this separation, and it hasn't seemed to impede me in any way, in fact it has opened more opportunities. Some examples of how this works: 1. Create an instance of the EventManager: var em = new EventManager(); 2. Register objects to listen for events in the event manager. You can either do this using the 'this' object, or by passing in an object. //assuming 'mouseMove' is a function inside the 'this' object. em.registerListener(this, "mouseMove", mouseMove); //assuming 'mousTracker' is an object somewhere with a //public 'mouseMove' function. em.registerListener(mouseTracker, "mouseMove", mouseTracker.mouseMove); 3. Raise events. //assuming 'rawDX' and 'rawDY' contain the change in value of mouseX and mouseY. em.raiseEvent("mouseMove", {dx:rawDX, dy:rawDY}); As you can see, we are passing in a &#106avascript object with the fields 'dx' and 'dy'. 4. In our callback function, we can use the optional data passed in. //this is the callback function in the 'mouseTracker' object. this.mouseMove = function(data){ alert("("+data.dx+", "+data.dy+")"); } 5. You can unregister listeners, even inside the callback function. You can either unregister each explicit callback function, or unregister all callback functions for that event type, for that object. //unregisters all 'mouseMove' listeners for 'this' object. em.unregisterListener(this, "mouseMove"); //unregisters the 'mouseMove' listener for &#111;nly the function 'yAxisMove' em.unregisterListener(this, "mouseMove", yAxisMove); //unregisters the 'mouseMove' listener for the 'mouseMove' function, from //within the 'mouseMove' callback. this.mouseMove = function(data){ alert("("+data.dx+", "+data.dy+")"); em.unregisterListener(this, "mouseMove", this.mouseMove); } I tried to make it as dynamic as possible, and cover most situations.
Advertisement
Quote:Original post by BUnzaga
I don't claim to be a &#106avascript guru, so I hope anyone who knows how to make this faster or better would post their findings.
Telling us which browser it works in would be useful; nothing happens in Opera 10, Internet Explorer 8 or Firefox 3 (well, IE 8 gets the little script error warning triangle).

What advantages would your manager give us over existing ones such as the one in MooTools?

[Website] [+++ Divide By Cucumber Error. Please Reinstall Universe And Reboot +++]

Hang on, this is more web-based &#106avascript? I was hoping you'd integrated a &#106avascript interpreter into a compiled game engine. :(
Oh dang! That is embarrassing.

It works great with google chrome. I had to rearrange some of the script, now it also is working in IE. Not sure about Mozilla, the boxes stay empty.

My main point wasn't to say my system is better than another system. In fact, it is probably far inferior. I don't do a lot of HTML coding, and I have never heard of MooTools. I was just hoping that someone starting out might get inspired or be able to see a working example and get their own ideas.

I personally had to do a lot of research, picking pieces of code from various languages and examples to construct this, I even posted a few questions on these forums about it.

It has worked wonders for the projects I have worked on, and has brought my programming experience to a new level. I am a big fan of event driven programming now, and just wanted to pass the enthusiasm on.

Sorry if it sounded like I was coming off cocky or like my system was the 'end all' system.
Quote:Original post by BUnzaga
It works great with google chrome. I had to rearrange some of the script, now it also is working in IE. Not sure about Mozilla, the boxes stay empty.
It's working in Opera 10 too, now. [smile]

Quote:My main point wasn't to say my system is better than another system. In fact, it is probably far inferior. I don't do a lot of HTML coding, and I have never heard of MooTools. I was just hoping that someone starting out might get inspired or be able to see a working example and get their own ideas.
It's certainly a valuable learning experience, especially if you find the results useful in your own project - sorry if I came across as harsh!

[Website] [+++ Divide By Cucumber Error. Please Reinstall Universe And Reboot +++]

Ow. Ow. Ow. Where to begin... ?

Quote:
EventManager._em = null;function EventManager(){
First, you're assigning a member to a function before defining that function, which is invariably going to hurt you (assigning a property to something that is not defined yet). If anything, you should have written:
function EventManager(){}EventManager._em = null;


Not that it matters, because what you implement here is a Monostate (a singleton would be achieved by making the constructor private). Besides, making the manager a singleton is much easier than conditionals in constructors:

var EventManager = (function(){  var self = function(){...};  return new self();})();


Quote:
if(EventManager._em == null)
This is equivalent to if(EventManager._em).

Quote:
	//helper function for Array object, more accurate than	function isNull(element, index, array){		return element == null;	}
if(isNull(foo,bar,qux)) is equivalent to if(foo) here, so why the function?

Quote:
this.registerListener = function(obj, e, callback){
Theoretically speaking, shouldn't you be adding functions to the prototype to improve readability? Also be careful of users who want to register non-member functions... maybe make the object argument optional by placing it in last position.
Quote:
		var j=listeners[e].length;		var i = 0;		for(; i < j; i++){
Sounds like a job for bicycle repair man for-in : for(var i in listeners[e]).

Besides, you could chain the registration function by returning this:
self.prototype.register = function(event, callback, obj){  var func = { obj : obj, callback : callback };  if (event in this.listeners) {    for (var i in this.listeners[event])       if (this.listeners[event].callback === callback &&          this.listeners[event].obj      === obj )        return;    this.listeners[event].push(func);  } else {    this.listeners[event] = [func];  }  return this;};


Quote:
		if(e in listeners){
Since the entire function body is contained within this conditional, you might want to use the linus trick (early-return if the condition is false).

Quote:
				for(var i = listeners[e].length-1; i > -1; i--){					if(listeners[e]['obj'] === obj){						listeners[e].splice(i, 1);					}				}
You've reinvented array filtering.

Quote:
if(listeners[e].every(isNull)){
Shouldn't this test be a simple if(!listeners[e].length) ?

Quote:
listeners[e] = null;delete listeners[e];
Why are you deleting null?

I would probably rewrite this as:

self.prototype.unregister(event,obj,callback){  if (!(event in this.listeners)) return this;    this.listeners[event] = this.listeners[event].filter(    callback       ? function(f){return f.obj !== obj || f.callback !== callback;}      : function(f){return f.obj !== obj;}  );  this.listeners = this.listeners.filter(    function(a){return a.length > 0;}  );  return this;};


Quote: /* Pass in the event string, and an optional 'data' object this could be an object, an array, a value, just about anything. */
Why not just pass an arbitrary number of arguments?

self.prototype.run = function(event){  if (!(event in this.listeners)) return this;  var handlers = this.listeners[event];  return function() {    for (var i in handlers) {      handlers.callback.apply(handlers.obj, arguments);    }    };};


On the other hand, I would rather have multiple instances of this system, since it lets me handle different behaviors in different parts of my code through dependency injection. The code amounts to:

[source type="javascript"]signals = (function(){  s = function() { this._c = s; };  s.prototype.channel = function(c) {    var h = [],        s = function() { for (var k in h) if (h[k]) h[k].apply(this,arguments); };    s.bind = function(f) { h.push(f); return h.length-1; };    s.unbind = function(f) { h[k] = null; };    return this.set(c,s);  };  s.prototype.set = function(n,v){    var i = function(){ this._c = i; };    i.prototype = new this._c();    i.prototype[n] = v;    return new i();  };  return s;})();


From here.
WOW. Just WOW.

Thank you for taking the time to post all this great information!

Most of what you see is what I have been able to hack and paste together using google and trial and error.

I'll be making a lot of modifications, and implementing a lot of what you suggested.
Hmm, you could probably use a reintroduction to &#106avascript.<br><br>While I'm linking to Mozilla, I should probably state that MDC may possibly be home to the internet's best reference documentation for &#106avascript, both for the language and web DOM. They even document deprecated IE features.
I haven't had a chance to work on this until tonight, but here is what I ended up with after taking a look at your suggestions:
var em = (function(){	var self = function(){		private:		listeners = {};		handlers = [];		}	// register listener you can pass in null, event, callback if you	// just want to run a callback function with no object	self.prototype.registerListener = function(o, e, c){		if(e in listeners){			for(var i in listeners[e]){				if(listeners[e].callback === c && listeners[e].obj === o){					return this;				}			}			listeners[e][listeners[e].length]={obj:o, callback:c};		}		else{			listeners[e] = [{obj:o, callback:c}];		}		return this;	}	// unregister pass in object (or null), event, and callback.  Leave the callback null	// if you want to remove all callbacks for this event.	self.prototype.unregisterListener = function(o, e, c){		if(!(e in listeners)){return this;}		if(c){			listeners[e] = listeners[e].filter(function(f){return f.obj!==o || f.callback!==c;});		}		else{			listeners[e] = listeners[e].filter(function(f){return f.obj!==o;});		}		if(listeners[e].length<1){delete listeners[e];}		return this;	}	// raise event, pass in event type and optional 'arguments'	self.prototype.raise = function(e){		if(!(e in listeners)){			return this;		}		handlers = listeners[e];		for(var i = 0, j; j=handlers;i++){			handlers.callback.apply(handlers.obj, arguments);		}		return this;	}	// debug utility	self.prototype.toString = function(){		var response = "<EventManager: \n  listeners:";		for(var i in listeners){			response +="\n    "+i;			for(var j = 0, k=listeners.length;j<k;j++){				response +="\n      "+listeners[j].callback.name;			}		}		return response+"\n>";	}	return new self();})();


For my purposes, I like passing in the object first, then it also coincides with the 'call' and 'apply' functions. If I don't want to tie it to an object, I can pass in null.

I also opted to make this a singleton as you were nice to show me an example of how to do it correctly. For my purposes, I will only need one of these globally, and if I need another one for a separate object, I can include it for that objects script and it will allow me to only create one per-object.

If I do find a reason why I will need more, I can just modify a few lines and now I can create as many 'new' ones as I need.

I wanted to make sure I knew what was going on with your array filter command, so I broke it up into two functions, just so I could wrap my head around it.

Anyway, thanks again for the help. I am very happy with it thus far. :)

This topic is closed to new replies.

Advertisement