90 days of node.js - Day 13 events

In case you don't know what this is all about, the out-of-loop link describing my 90 day journey through node land.

Sidenote - I disappeared for a month and a half taking some summer time off with my family. Spending even an hour with them on a weekday reduces my time and energy available for typing a long technical article. Ergo long pause. I expect that these articles will get more spaced apart and won't be a daily thing, which is probably the best thing for me.

Now that that is out of the way, onward.

As I've been writing these posts, I realised that a lot of them are narrowly focussed discussion on some module of the node ecosystem. I wanted to take a step back and work on one specific, core concept. What can be more core than events.

Why do I need events?

node has an async view of the world. It's running on the single thread that v8 gives, but it relies on async callbacks. That leads to some interesting code patterns like callback hell. The potential issues with this approach are twofold. One, the code becomes extremely difficult to read. Nested callbacks are difficult to decode. Add closures to the mix and things can quickly become difficult to unravel. Secondly, you can end up having extremely tightly coupled code where every part of the code base is either aware of some global variable or we have to design the methods on each object to have extra arguments that indicate state.

For smaller applications, neither of these are a big deal. It's when you start onboarding more developers and writing more lines of code that this becomes a much bigger problem. It not only impacts the quality of code, but in my experience, teams start unraveling as people reach conflicting positions on what the right approach. At best there is passive aggressive snark, at worst the St. Valentine's day massacre.

That is hardly a way to run a business or even a pet project.

Enter, events

In their most basic form, events are notifications that something of significance has happened. That "something" is usually encoded as a word which describes the happening e.g. "change", "error" etc. Typically, events are emitted (or triggered) by objects which run code causing significant happenings. These events can also be accompanied by data which provides context around the event which is from the emitting object.

On the other side of the emitting object are event listeners - blocks of code listening to the event being fired. Because of the single threaded nature of node.js, these listeners are in the same process; but they can be in different modules. These listeners react to the event and perform some business logic.

A good analogue of this behavior is the Observer pattern.

One minor thing to note - node.js events are not the same as DOM events. DOM events fire in the browser, node events fire in the node process. I've always heard confusion on this point, especially from devs who are not used to events and it can get disconcerting quickly.

A quick note on language - I use fire or trigger interchangeably with emit because I've heard it used like that before. I am not quite sure what the history behind these words is, but would be good to find out.

Events in node

node provides events through the events module. Let's just jump into some code to explain how events work.

Emitting and listening
By far the most common use case with events is to emit them, so interested portions of the code can be notified and listening to any interesting events. This gist demos some of that code.

It's pretty busy but only because I am demoing quite a few things. Lines 2 and 4 are where you end up creating a new EventEmitter object. This is the magic sauce of all node.js events (if the reference wasn't already clear :) ). A common mistake is to forget getting an instance of this class. There are very few non-instance members available, so always get an instance.

Skipping all the way down to line 26, we find a call to the emit method. This is what all any emitter of events does. For example, the really helpful 'data' event that a readable stream might emit, is fired this way. Typically you will pass the second argument to the 'emit' method, containing context about who fired the event and where it was fired from. This can be an object or a string. Objects are probably better though primitives can be easier to work with at times.

Naming is of course one of the harder things about writing code. Be careful when naming events. We tend to name events as commands e.g. "turnOnButton", "flushTheQueue". Try and avoid that. You want your event to state exactly what has happened NOT the expected consequence. Otherwise, other portions of the code (which are not buttons or queues for example) will have to add listeners to oddly named events. It can lead to quite a bit of confusion and unnecessary argument. #truestory

Now, that you know how to fire an event, let's listen to it. There are 2 ways to listen to an event - using 'on' or using 'addListener'. There is no real difference between each method and you can use either. I prefer using 'on' primarily because it is a habit from jQuery. Lines 12 and 17 demonstrate both methods.

A cool thing about both the emit and on/addListener methods is that they return the instance of the emitter. This is great for chaining different event listeners to the same emitter. This makes for some nice and compact code which is easy to navigate. Line 19 demonstrates a chained call. Chained calls are a personal preference - they are very functional, but debugging them (especially for developers who are unfamiliar with stack traces) can be tricky.

Adding a new listener fires the (wait for it) "newListener" event. This is a really useful event, especially during debugging, which can help us catch any rogue event listeners.
Now that you've added listeners, you need a way to remove them. Any time a component which added a listener is removed from the code, make sure you remove the listener. This is so basic, that everyone forgets this sooner or later. Here's some sample code.

Line 15 demonstrates removal of the event listener. Even if an event is fired after this call, any removed listeners will not be triggered. Other listeners may fire. There is no way I know of to cause an event to fire and have it's listeners ignored. There might be some cases for code like that, but node doesn't support them.

Of course, if you want to remove all listeners which can be a common use case in UI design, use the removeAllListeners method on your emitter instance. This really helps if you have no control on when and where your listeners are getting attached.

Max Listeners 
Knowing how many listeners on your event is a very useful feature. This gives us info on listeners added unintenionally.  Other than the newListeners event, node provides a few other helpers. This gist demonstrates them.

On adding a largish number of listeners, node helpfully prints out an error on the console.error. By default, this number is 10. You can set it to any number you want with the setMaxListeners method call on your emitter instance. You can also set it on *all* emitter instances - present and future - using the defaultMaxListeners property. This is a little opinionated call and will invariably lead to message relays and other workarounds. Better to set it on the instance.

The other way to determine the listeners on an event is to call the listeners method on your emitter instance. This will return a list of Function objects that are attached to the event. Being a Function, you must override the toString to get anything useful out of these functions.

Domains are responsible for managing context of an operation in node. Typically, you want to attach the emitter to the domain making it easy to detach it if you wanted to. You can attach the instance of the domain to an event emitter, but removing the link can be a little odd.

So that's events. Use them wisely, use them as required to communicate between different part of the code base and they can make code a lot easier to write and think about. Start emitting events for every single operation where another method call would suffice in the same object and you'll end up with more spaghetti than you can eat.

If you found something I missed, or you want me to talk about something else, please leave a comment :)

No comments:

Post a Comment