When an object has a PubSub interface, we call it an evented object. A special case is when an object used to store data (a model) publishes events whenever its contents are modified. Models are the M in Model-View-Controller (MVC), which has become one of the hottest topics in JavaScript programming in the last few years. The core concept is that MVC applications are data-centric so that model events impact the DOM (aka the View) and the server (via the Controller).
Let’s look at the hugely popular Backbone.js framework.[29] You create a new model like so:
| style = new Backbone.Model( |
| {font: 'Georgia'} |
| ); |
model just represents the simple object that was passed in.
| style.toJSON() // {"font": "Georgia"} |
But unlike an ordinary object, this one publishes notifications when a change is made.
| style.on('change:font', function(model, font) { |
| alert('Thank you for choosing ' + font + '!'); |
| }); |
Old-school JavaScript made changes to the DOM directly from input event handlers. New-school JavaScript makes changes to models, which then emit events that cause the DOM to update. In nearly all apps, this separation of concerns results in more elegant, intuitive code.
In its simplest form, MVC consists of wiring models to views: “If this model changes this way, change the DOM that way.” But the biggest gains from MVC happen when change events bubble up the data tree. Instead of subscribing to events on every leaf, you can just subscribe to the roots and branches.
To that end, Backbone models are often organized into Backbone collections, which are essentially evented arrays. You can listen for when models are added to and removed from them. Backbone collections automatically propagate events from the models they contain.
For example, you might have a spriteCollection object containing hundreds of models representing things you’re drawing on a canvas element. Each time any of those sprites change, you need to redraw the canvas. Rather than attaching the redraw function as a handler for the change event on each sprite individually, you could instead just write the following:
| spriteCollection.on('change', redraw); |
Note that this automatic propagation goes only one level down. Backbone has no notion of nested collections. However, you can implement this propagation yourself using Backbone’s trigger method. With it, any Backbone object can emit arbitrary events.
Propagating events from one object to another poses certain concerns. If an event on one object causes a series of events that will ultimately trigger the same event on the same object every time, then the result will be an event cycle. And if the cycle is synchronous, the result will be a stack overflow, like we saw in Synchronicity.
Yet oftentimes, a cycle of change events is exactly what we want. The most common case is a two-way binding, where two models have interrelated values. Suppose we want to ensure that x always equals 2 * y.
| var x = new Backbone.Model({value: 0}); |
| var y = new Backbone.Model({value: 0}); |
| x.on('change:value', function(x, xVal) { y.set({value: xVal / 2}); }); |
| y.on('change:value', function(y, yVal) { x.set({value: 2 * yVal}); }); |
You might expect this code to lead to an infinite loop the moment the value of x or y is changed. But actually, it’s quite safe, thanks to two safeguards in Backbone.
set doesn’t emit a change event if the new value matches the old one.
Models can’t emit a change event during one of its own change events.
The second safeguard presents gotchas of its own. Suppose a change is made to a model that results in a second change to the same model. Because the second change is “nested” in the first one, it’ll occur silently. Observers won’t have a chance to respond to it.
Clearly, maintaining two-way data bindings in Backbone is a challenge. Another major MVC framework, Ember.js, takes a different approach: two-way bindings are declared explicitly. When one value changes, the other is updated asynchronously from a timeout event. So, until that event fires, the application’s data may be in an inconsistent state.
There’s no easy solution to the problem of bindings across evented models. In Backbone, a prudent way to step around the issue is the silent flag. If you add {silent: true} to a set event, no change event will happen. So, if several entangled models need to be updated at once, a good approach is to set them silently. Then call their change methods to fire the appropriate events only after they’re in a consistent state.
Evented models give us an intuitive way of transforming application state changes into events. Everything Backbone and other MVC frameworks do is about these models, updating the DOM and the server when their states change. Storing mutable data in evented models is a great first step to reigning in the growing complexity of client-side JavaScript applications.
3.139.97.161