JavaScript Observer (Publish/Subscribe) Pattern
That’s the pattern that I love. It seems to be a core part of JavaScript. And it gives ability of quite many ways of implementing it depending on requirements.
Definition
The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.
Taking in account previous patterns this pattern adds one more principle:
- Strive for loosely coupled designs between objects that interact
So the main idea is that we have one main object to which you subscribe (Subject/Observable) and a lot of objects (Observers) that subscribe and wait for events.
One very important thing to remember is that objects may receive messages in random order, so you shouldn’t rely on the order in which you added Observers.
Basic Observer Example
What will follow is not the simplest example. The simplest example would be the one that is not passing the message.
var Observable = {
observers: []
, addObserver: function(observer) {
this.observers.push(observer)
}
, removeObserver: function(observer) {
var index = this.observers.indexOf(observer)
if (~index) {
this.observers.splice(index, 1)
}
}
, notifyObservers: function(message) {
for (var i = this.observers.length - 1; i >= 0; i--) {
this.observers[i](message)
};
}
}
Observable.addObserver(function(message){
console.log("First observer message:" + message)
})
var observer = function(message){
console.log("Second observer message:" + message)
}
Observable.addObserver(observer)
Observable.notifyObservers('test 1')
// Second observer message:test 1
// First observer message:test 1
Observable.removeObserver(observer)
Observable.notifyObservers('test 2')
// First observer message:test 2
If you want to remove observers using some sort of ID instead of passing callback it can be done in following way:
var Observable = {
observers: []
, lastId: -1
, addObserver: function(observer) {
this.observers.push({
callback: observer
, id: ++this.lastId
})
return this.lastId
}
, removeObserver: function(id) {
for (var i = this.observers.length - 1; i >= 0; i--) {
this.observers[i]
if (this.observers[i].id == id) {
this.observers.splice(i, 1)
return true
}
}
return false
}
, notifyObservers: function(message) {
for (var i = this.observers.length - 1; i >= 0; i--) {
this.observers[i].callback(message)
};
}
}
var id_1 = Observable.addObserver(function(message){
console.log("First observer message:" + message)
})
var observer = function(message){
console.log("Second observer message:" + message)
}
var id_2 = Observable.addObserver(observer)
Observable.notifyObservers('test 1')
Observable.removeObserver(id_2)
Observable.notifyObservers('test 2')
Pull vs Push
There are two main strategies for observer pattern:
- Push behaviour - when an event happens Observable object will notify all Observers by sending all the new data to them
- Pull behaviour - when an event happens Observable object will notify all Observers and each Observer will pull the information it needs from the Observable
Pull method is more preferable as in this case you’ll ask only for data that you need. Otherwise at some point you Observable may send huge objects with a lot of attributes. In this example Observable will only notify Observers that something changed and each Observer will take the data it needs. Also in this example we hide observers array and private values in anonymous function closure.
var Observable = {}
;(function(O){
var observers = []
, privateVar
O.addObserver = function(observer) {
observers.push(observer)
}
O.removeObserver = function(observer) {
var index = observers.indexOf(observer)
if (~index) {
observers.splice(index, 1)
}
}
O.notifyObservers = function() {
for (var i = observers.length - 1; i >= 0; i--) {
observers[i].update()
};
}
O.updatePrivate = function(newValue) {
privateVar = newValue
this.notifyObservers()
}
O.getPrivate = function() {
return privateVar
}
}(Observable))
Observable.addObserver({
update: function(){
this.process()
}
, process: function(){
var value = Observable.getPrivate()
console.log("Private value is: " + value)
}
})
Observable.updatePrivate('test 1')
// Private value is: test 1
Observable.updatePrivate('test 2')
// Private value is: test 2
Observer with topics
In order not to create multiple observable objects it is much better to add topic functionality to Observer pattern. In simplest form it may look like:
var Observable = {
observers: []
, addObserver: function(topic, observer) {
this.observers[topic] || (this.observers[topic] = [])
this.observers[topic].push(observer)
}
, removeObserver: function(topic, observer) {
if (!this.observers[topic])
return;
var index = this.observers[topic].indexOf(observer)
if (~index) {
this.observers[topic].splice(index, 1)
}
}
, notifyObservers: function(topic, message) {
if (!this.observers[topic])
return;
for (var i = this.observers[topic].length - 1; i >= 0; i--) {
this.observers[topic][i](message)
};
}
}
Observable.addObserver('cart', function(message){
console.log("First observer message:" + message)
})
Observable.addObserver('notificatons', function(message){
console.log("Second observer message:" + message)
})
Observable.notifyObservers('cart', 'test 1')
// First observer message:test 1
Observable.notifyObservers('notificatons', 'test 2')
// Second observer message:test 2
More advanced versions may have features like:
- Subtopics (e.x. /bar/green or bar.green)
- Publishing to topic propagates to subtopics
- Publishing to all topics
- Giving a priority to subscribers
Observer Pattern using jQuery.Callback
jQuery has quite a nice feature like $.Callback. Besides of classical Observer functionality it also has a set of flags:
- once: Ensures the callback list can only be fired once (like a Deferred)
- memory: Keeps track of previous values and will call any callback added after the list has been fired right away with the latest "memorized" values (like a Deferred).
- unique: Ensures a callback can only be added once (so there are no duplicates in the list).
- stopOnFalse: Interrupts callings when a callback returns false.
Using this options your can customise your Observer in quite interesting ways. Lets see the most simple example using jQuery.Callback:
var callbacks = jQuery.Callbacks()
, Topic = {
publish: callbacks.fire,
subscribe: callbacks.add,
unsubscribe: callbacks.remove
}
function fn1( value ){
console.log( "fn1: " + value );
}
function fn2( value ){
console.log("fn2: " + value);
}
Topic.subscribe(fn1);
Topic.subscribe(fn2);
Topic.publish('hello world!');
Topic.publish('woo! mail!');
If you want to see a more advanced example with topic than take a look at this [1a] example.
CoffeeScript example
This is a simple example without topics. Almost the same example (at least the same amount of lines) can be found in CoffeeScript Cookbook [7].
class Observable
constructor: () ->
@subscribers = []
subscribe: (callback) ->
@subscribers.push callback
unsubscribe: (callback) ->
@subscribers = @subscribers.filter (item) -> item isnt callback
notify: () ->
subscriber() for subscriber in @subscribers
class Observer1
onUpdate: () ->
console.log "1st got new message"
class Observer2
onUpdate: () ->
console.log "2nd updated"
observable = new Observable()
observer1 = new Observer1()
observer2 = new Observer2()
observable.subscribe observer1.onUpdate
observable.subscribe observer2.onUpdate
observable.notify()
Sources
- (github) shichuan / javascript-patterns / design-patterns / observer.html and jQuery examples
- (book) Head First Design Patterns
- (book) Learning JavaScript Design Patterns
- (book) JavaScript Patterns: Build Better Applications with Coding and Design Patterns
- (book) Learning JavaScript Design Patterns: A JavaScript and jQuery Developer's Guide
- (book) Pro JavaScript Design Patterns: The Essentials of Object-Oriented JavaScript Programming
- (book) CoffeeScript Cookbook
- (article) dofactory JavaScript Observer Pattern