Pub-Sub and Event-Driven Programming
Event-driven programming is incredibly important in software engineering, and it's the foundation of JavaScript (both in the browser and Node.js). Events are emitted on interaction either by a user or triggered from within the program, and there are listeners that execute a callback when they recognize a specific event.
You can see instances of event-driven programming in:
- Browser events (
onclick
,onload
,onkeyup
, etc.) - Node.js server events
- Sockets
- Messaging
- Redux and other state management libraries
- RxJS and other event libraries
- GraphQL subscriptions
There are different names for the various event design patterns, but the core concept is the same. In this lesson, we will build a Pub-Sub, but the intuition will be transferable. Other similar concepts are:
- Observer pattern
- Event bus
- Message queue
- Event-driven architecture
There are subtle differences between the different pattern implementations, but the important learning outcomes are all the same. In this question, you will build a Pub-Sub (publish-subscribe pattern) where publishers emit actions and subscribers listen for a specific action and execute a callback accordingly.
We will implement the Pub-Sub as a class with subscribe
, publish
, unsubscribe
methods.
They will communicate through an eventName
channel which is just a string.
It should be built so that many subscribers
can listen for the same event.
Then to use it, we do the following:
Pub-Sub Implementation
Our Pub-Sub implementation will give us the ability to subscribe
to an event, publish
data to an event, and a place to store the subscribed callbacks.
In the constructor, we initialized a subscriptions
object whose key-value pairs will be the event channel which maps to an array of event callbacks.
subscribe
method
The first thing we should implement is the subscribe
method.
The function takes an eventName
which is a string, and a callback
which is a function that executes with the data.
First we check if an array of callbacks in subscribers
already exists for the eventName
channel, and if it doesn't exist, we initialize it now.
Then we push the callback
to this array which is a function that takes a single argument.
publish
method
When we want to trigger an event, we publish
under the eventName
and pass it the data
for that event.
Our callbacks should take at most one argument, but it could take no arguments if it does not use the data
.
In our case, the data
can be anything, but you could enforce it to be something specific either in your Pub-Sub implementation or by the callback
for a specific eventName
.
When we publish
an event, we first check that the eventName
has been initialized and return early if it has not.
If it does exist, we iterate through all the subscribers
for that eventName
and execute their callbacks.
unsubscribe
method
We want to allow our code to clean up a subscription and remove its listener.
We pass an eventName
and the same callback
from the subscribe
method to remove it.
Then the callback is filtered out of our subscribers
for that channel.
Functions are stored in memory as in JavaScript. You must pass the exact same function you used originally which means you'll need to store it in a variable. See how this is done in the solution REPL.
Full Implementation
Interact with this solution in a REPL.
Wrap Up
The publish-subscribe pattern is actually simple to implement, but the usage is very powerful. This is a common JavaScript interview question because it shows an understanding of an essential pattern and knowledge of fundamental JS concepts.
One challenge developers have with a Pub-Sub is the terminology because it makes it sound much more challenging than it actually is. When we say a subscriber is "listening", all this means is that we have a hash table (object) that has an array of events for a given key. A subscriber adds its callback to this array, and the publisher just executes all the callback under that key when it "emits" the matching event.
If you have used Redux, what's happening underneath the hood is almost the exact same.
The only difference is that we are storing state inside our Pub-Sub, and the eventName
(action.type
now)
is passing the action to every reducer, and they decide if they should update a piece of state based on the action.
Below is a simple implementation with phrases changed to Redux terminology:
For larger system Pub-Subs, message queues, and observers, it requires more robust storage and communication channels, but the fundamentals are still the exact same. Maintain channel names and a list of functions to execute.