Observers
edit this pageLike publish/subscribe, but different
A common pattern in modern JavaScript is to make models observable, using the traditional publish/subscribe mechanism.
For example, you can observe changes to attributes within a Backbone Model like so:
model = Backbone.Model({ myValue: 1 });
model.on( 'change:myValue', function ( model, value, options ) {
alert( 'myValue changed to ' + value );
});
model.set( 'myValue', 2 ); // alerts 'myValue changed to 2'
This works because Backbone.Model.prototype
inherits from Backbone.Events
.
Ractive implements pub/sub with ractive.on(), ractive.off() and ractive.fire() - see Events for more info.
Observing models with nested properties
But the normal pub/sub mechanism won't work for monitoring data changes with Ractive, because our data can contain nested properties. It's no good subscribing to a change:foo.bar
event, if foo.bar
can change as a result of foo
changing.
So instead, we introduce the concept of observers.
An observer observes a particular keypath, and is notified when the value of its keypath changes, whether directly or indirectly (because an upstream or downstream keypath changed). You create one with ractive.observe()
.
Here's an example:
ractive = new Ractive({
el: myContainer,
template: myTemplate,
data: {
foo: { bar: 1 }
}
});
// The observer will be initialised with ( currentValue, undefined ) unless
// we pass a third `options` argument in which `init` is `false`. In other
// words this will alert 'foo.bar changed to 1'
observer = ractive.observe( 'foo.bar', function ( newValue, oldValue, keypath ) {
alert( keypath + ' changed to ' + newValue );
});
ractive.set( 'foo.bar', 2 ); // alerts 'foo.bar changed to 2'
ractive.get( 'foo' ); // returns { bar: 2 }
ractive.set( 'foo', { bar: 3 }); // alerts 'foo.bar changed to 3'
ractive.get( 'foo.bar' ); // returns 3
observer.cancel();
ractive.set( 'foo.bar', 4 ); // alerts nothing; the observer was cancelled
Observers are most useful in the context of two‐way binding.
A 'gotcha' to be aware of
Observers will be notified whenever the new value is not equal to the old value - sort of.
What does 'not equal' mean? Well, with primitive values such as strings and numbers, that's easy - they're either identical (in the ===
sense) or they're not.
With objects and arrays (hereafter, just 'objects', since that's what arrays technically are), it's not so straightforward:
a = { one: 1, two: 2, three: 3 };
b = { one: 1, two: 2, three: 3 };
alert( a === b ); // alerts 'false' - they look the same, but they ain't
b = a;
b.four = 4;
alert( a === b ); // alerts 'true'. Hang on, `a` didn't have a 'four' property?
alert( a.four ); // alerts '4'. Oh. Right.
So one the one hand, objects which look identical aren't. On the other, you can set a property of an object and have no idea whether doing so resulted in a change.
There are two possible responses to this problem. First, we could do a 'deep clone' of an object whenever we do ractive.set(keypath, object)
, using an algorithm similar to jQuery extend. That would mean any references you held to object
would become irrelevant. It would also mean a whole load of extra computation, and probably some very strange behaviour with cyclical data structures. No thanks.
The second is to sidestep the issue, and simply state that for the purposes of determining whether to notify observers, no two objects are equal, even when they're identical (unless they're both null
, of course - since typeof null === 'object'
due to a bug in the language).
This is the safest, sanest behaviour, but it can lead to unexpected behaviour in one situation - accessing properties within an observer:
obj = { a: { b: { c: 1 } } };
ractive = new Ractive({
el: myContainer,
template: myTemplate,
data: { obj: obj }
});
// We observe 'obj.a.b.c' indirectly, and directly
ractive.observe({
'obj': function ( newObj, oldObj ) {
alert( 'Indirect observer: changed from ' + oldObj.a.b.c + ' to ' + newObj.a.b.c );
},
'obj.a.b.c': function ( newC, oldC ) {
alert( 'Direct observer: changed from ' + oldC + ' to ' + newC );
}
});
obj.a.b.c = 2;
// The next line will cause two alerts:
// 'Direct observer: changed from 1 to 2'
// 'Indirect observer: changed from 2 to 2' - because oldObj === newObj
ractive.set( 'obj', obj );
This is definitely an edge case, but one that it's worth being aware of.