The Publish/Subscribe Pattern in JavaScript

Christopher T.

October 10th, 2019

Share This Post

In this article, we will be going over the publish/subscribe pattern in JavaScript and see how simple (but powerful) it is to implement in our JavaScript applications.

The publisher/subscriber pattern is a design pattern that allows us to create powerful dynamic applications with modules that can communicate with each other without being directly dependent on each other.

The pattern is quite common in JavaScript and has a close resemblance to the observer pattern in the way it works, except that in the observer pattern, an observer is notified directly by its subject whereas in the publisher/subscriber the subscriber is notified through a channel that sits in between the publisher and subscriber that relays the messages back and forth.

When we implement this, we will need a publisher, subscriber, and some place to store callbacks that are registered from subscribers.

Let's go ahead and see how this looks like in code. We're going to use a factory function (you don't have to use this pattern) to create the publisher/subscriber implementation.

The first thing we are going to do is to declare a local variable inside the function to store subscribed callbacks:

function pubSub() {
  const subscribers = {}
}

Next, we'll define the subscribe method which will be responsible for inserting callbacks to subscribers:

function pubSub() {
  const subscribers = {}

  function subscribe(eventName, callback) {
    if (!Array.isArray(subscribers[eventName])) {
      subscribers[eventName] = []
    }
    subscribers[eventName].push(callback)
  }

  return {
    subscribe,
  }
}

What's happening here is that before attempting to register a callback listener for an event name, it checks to see if the eventName property in the subscribers storage is already an array. If it isn't it assumes that this will be the first registered callback for subscribers[eventName] and initializes it into an array. Then, it proceeds to push the callback into the array.

When the publish event fires, it will take two arguments:

  1. The eventName
  2. Any data that will be passed to *every single callback registered in subscribers[eventName]

Lets go ahead and see how this looks like in code:

function pubSub() {
  const subscribers = {}

  function publish(eventName, data) {
    if (!Array.isArray(subscribers[eventName])) {
      return
    }
    subscribers[eventName].forEach((callback) => {
      callback(data)
    })
  }

  function subscribe(eventName, callback) {
    if (!Array.isArray(subscribers[eventName])) {
      subscribers[eventName] = []
    }
    subscribers[eventName].push(callback)
  }

  return {
    publish,
    subscribe,
  }
}

Before iterating on the list of callbacks in subscribers, it will check if it actually exists as an array in the object and if it doesn't it will assume that the eventName was never even registered before so it will simply just return. This is a safeguard against potential crashes. After that, if the program reaches the .forEach line then we know that the eventName was registered with one or more callbacks in the past and proceeds to loop through subscribers[eventName] safely. For each callback that it encounters, it calls the callback with the data that was passed in as the second argument.

So if we subscribed a function like this:

function showMeTheMoney(money) {
  console.log(money)
}

const ps = pubSub()

ps.subscribe('show-money', showMeTheMoney)

And call the publish method sometime in the future:

ps.publish('show-money', 1000000)

Then the showMeTheMoney callback we registered will be invoked in addition to receiving 1000000 as the money argument:

function showMeTheMoney(money) {
  console.log(money) // result: 10000000
}

And that's how the publisher/subscriber pattern works! We defined a pubSub function and provided a location locally to the function that stores the callbacks, a subscribe method to register the callbacks, and a publish method that iterates and calls all of the registered callbacks with any data.

There's one more problem though. In a real application we might suffer a never ending memory leak if we subscribe many callbacks, and it's especially wasteful if we don't do anything about that.

So what we need last is a way for subscribed callbacks to be removed when they are no longer necessary. What often happens in this case is that some unsubscribe method is placed somewhere. The most convenient place to implement this is the return value from subscribe, because in my opinion it is the most intuitive when we see this in code:

function subscribe(eventName, callback) {
  if (!Array.isArray(subscribers[eventName])) {
    subscribers[eventName] = []
  }
  subscribers[eventName].push(callback)
  const index = subscribers[eventName].length - 1

  return {
    unsubscribe() {
      subscribers[eventName].splice(index, 1)
    },
  }
}

const unsubscribe = subscribe('food', function(data) {
  console.log(`Received some food: ${data}`)
})

// Removes the subscribed callback
unsubscribe()

In the example, we needed an index so that we make sure that we remove the right one since we used .splice which needs an accurate index to remove the item we are looking for.

You can also do something like this, however it's less performant:

function subscribe(eventName, callback) {
  if (!Array.isArray(subscribers[eventName])) {
    subscribers[eventName] = []
  }
  subscribers[eventName].push(callback)
  const index = subscribers[eventName].length - 1

  return {
    unsubscribe() {
      subscribers[eventName] = subscribers[eventName].filter((cb) => {
        // Does not include the callback in the new array
        if (cb === callback) {
          return false
        }
        return true
      })
    },
  }
}

Disadvantages

Although there are huge benefits to this pattern, there are also devastating disadvantages that might cost us a lot of debugging time. How do we know if we subscribed the same callback before or not? There's really no way to tell unless we implement a utility that maps through a list, but then we'd be making JavaScript do more tasks.

It also becomes harder to maintain our code the more that we abuse this pattern in a real world scenario. The fact that callbacks are decoupled in this pattern makes it hard to track down each step when you have callbacks doing this and doing that everywhere.

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!


Tags


Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2020