View
 

Runtime-Key Value Observing

Page history last edited by Byron Ruth 14 years, 6 months ago

In the previous section, you learned about Key Value Coding (KVC), which provides a generic way for you to access and modify properties on an object without writing accessor methods.    Key Value Observing (KVO) provides a generic way to notify parts of your application when property values change.

 

You should use KVO in your application to eliminate much of the glue code you might typically write to keep the various parts of your application in sync.

 

A Simple Example

 

Traditionally, programmers think of their applications as a bundle of functions or methods that act on some data.  For example, if you are writing a preferences application and you want to change some data and then notify the server, you would typically write something like this:

 

MyApp.preferencesController = SC.Object.create({

 

  sendChangeToServer: function(key) {

    // .. send key/value pair to server

  }

});

 

// TO MODIFY YOUR CONTROLLER:

MyApp.preferencesController.set('showFullName', YES);

MyApp.preferencesController.sendChangesToServer('showFullName');

 

With this design, anytime you change the 'showFullName' property using an accessor, you need to also call sendChangesToServer() to notify the server.  This approach may work initially, but soon the drawbacks becomes pretty obvious.  For one thing, every time you need to send changes to the server, you will need to remember to call this method.  Not only is it easy to forget, but the rest of your code will be more complicated as well.

 

Most developers next start to package repeated calls into convenience methods like so:

 

 

MyApp.preferencesController = SC.Object.create({

 

  sendChangeToServer: function(key) {

    // .. send key/value pair to server

  },

 

  setAndSendToServer: function(key, value) {

    this.set('showFullName', YES);

    this.sendChangesToServer('showFullName');

    return this;

  } 

});

 

// TO MODIFY YOUR CONTROLLER:

MyApp.preferencesController.setAndSendToServer('showFullName', YES);

 

Of course, before long you will end up with dozens of convenience methods that must all be called differently depending on the circumstance.  You also lose the benefits of universal accessors.

 

With KVO, instead of thinking about methods, you should instead think of properties and what needs to happen  when a property changes.  In the example above, the state is the "showFullName" property.  Whenever that state changes, we need to send the change to the server.  Here is how you could write the same code using observers:

 

MyApp.preferencesController = SC.Object.create({

 

  showFullName: NO,

 

  sendChangesToServer: function(key) {

    // ... send changes to the server

  }.observes('showFullName')

});

 

// TO MODIFY YOUR CONTROLLER

MyApp.preferencesController.set('showFullName', YES);

 

This is clean code.  Not only is it simpler, but now the rest of your application can be written to simply modify properties on your object without having to know anything about what goes on behind the scenes when those property changes.  

 

Well designed SproutCore applications are built almost entirely on KVO.  It is fairly rare to write methods that will be called directly.  You will know you are writing your applications properly when the code begins to disappear from your app. 

 

Adding an Observer

 

You can add an observer manually to any object by using the addObserver() method defined in SC.Observable (and inherited into SC.Object).  Usually, however, you will setup observers implicitly when you define your classes using the observes() helper method.  To understand how observes() works, however, it helps to learn addObserver() first:

 

Using addObserver()

 

You can observe any property on any object by simply calling object.addObserver().  The first parameter you pass to this method should be a key name describing the property you want to observe.  Several options are possible here, but the most common form involves simply naming the property you want to observe.  For example:

 

var contact = SC.Object.create({

  firstName: "John",

  lastName: "Doe"

});

 

contact.addObserver("firstName", function() {

  console.log('firstName did change!');

});

 

contact.set('firstName', 'Jane');

> firstName did change!;

 

In this case, SproutCore will automatically call your observer function anytime the firstName property changes on the contact object.  Note that your observer will only be called if the firstName value changes, not if a property on the firstName value changes.  For example, let's say we added this contact as a property of a "user" object:

 

var user = SC.Object.create({

  contact: contact

});

 

user.addObserver("contact", function() {

  console.log('contact did change');

});

 

contact.set('firstName', 'Jana');

> firstName did change!

 

user.set('contact', anotherContact);

> contact did change!

 

In this example the observer we added earlier to contact fire  by the observer we just added to the "user" object did not fire.  This is because we change firstName, which is a property of the contact object.  The contact object itself did not change.   Later, when the contact property of the user actually changed, that observer fired instead.

 

Chained Observers

 

What if you were writing your user object and you actually cared about the contact.firstName property, not just contact?  For this case you can use chained observers instead.  To add a chained observer, you simply name multiple properties when you call add observer like so:

 

user.addObserver('contact.firstName', function() {

  console.log('contact.firstName did change');

});

 

contact.set('firstName', 'Jana');

> firstName did change!

> contact.firstName did change!

 

user.set('contact', anotherContact);

> contact did change!

> contact.firstName did change!

 

In this case, changing the firstName properties caused both the simple observer we added in the previous section and the chained observer to fire.  Interestingly, when we changed the contact object, this observer fired as well.  

 

This is because chained observers actually monitor changes not just to a single property but to every property in the path that you name.  In fact, internally, chained observers are implemented by creating multiple simple observers on each property in the path.  Whenever one of these observers are triggered, they will notify any of their children in a chain, eventually calling your observer at the end - hence the name.

 

Chained observer are an incredibly useful way to flatten out your object hierarchy.  You can observe properties several levels deep and be notified whenever any object in the hierarchy changes.  

 

As useful as this feature can be, be wary when using chained observers.  Chained observers are considerably more complicated than simple observers; hence they will impose a small performance cost on your app.  You can usually use them frequently in your model layer, and for simple views, but you should avoid creating hundreds or thousands of them.  

 

In particular, you may want to avoid them when designing list items to display in collection views.

 

Chained Observers and Computed Properties. You have have noticed that property observing looks in many ways similar to computed properties introduced in the last section.  This is true but at least one important difference is that chained observers are only used by observers, not by computed properties.  The property() method for a computed property can only accept simple property keys; not chained property paths like addObserver() or observes().

 

Using a Target

 

The examples we've shown so far add a simple callback function for your observers.  In fact, you will rarely actually use observers in this way.  Most of the time, functions are used in JavaScript as methods on an object.  When the function is run, you usually want the "this" variable to point to your owner object - not the "window" object as the above examples would do.

 

Many JavaScript libraries have a .bind() or similar method that can be used to force a function to be called in a particular context.  SproutCore prefers that you avoid doing this because it requires creating closures, which can hurt the performance of your application.

 

Instead, most SproutCore APIs that accept a callback function (like this one) will take a "target" object as well, which will be used as the calling context.  For example, here is how you could setup an observer so that it is always called as a method of the user object:

 

var user = SC.Object.create({

  contact: contact,

 

  contactFirstNameDidChange: function() {

    console.log('contact.firstName did change in: ' + this);

  }

});

 

user.addObserver('contact.firstName', user, user.contactFirstNameDidChange);

 

contact.set('firstName', 'Jane');

> contact.firstName did change in: SC.Object<sc123>

 

The observer was invoked with its "this" property set to the user object.  Most of the time, this is how you will want to use observers - and most other callbacks in SproutCore.

 

One other benefit of passing a target object this way is that you can also pass a method name instead of an actual function for the third parameter.  For example, the addObserver() call above could easily have been:

 

user.addObserver('contact.firstName', user, 'contactFirstNameDidChange');

 

When the observer fires, SproutCore will automatically lookup the contactFirstNameDidChange property on the user object and call it.  This is useful for many metaprogramming techniques.

 

Removing an Observer

 

Briefly before we move onto the observes() helper, it is useful to know how to remove an observer.  You will rarely actually need to do this in real applications, but it is useful to understand how it works.

 

To remove an observer you have previously registered, simply call removeObserver() passing the same key, target and method value as you did before.

 

contact.addObserver('firstName', contact, 'firstNameDidChange');

contact.set('firstName', 'John');

> observer fires

 

contact.removeObserver('firstName', contact, 'firstNameDidChange');

contact.set('firstName', 'Jana');

- observer does not fire

 

Adding an Observer with observes()

 

The addObserver()/removeObserver() primitives are useful, especially when building objects dynamically.  Most of the time, however, you will define classes rather than building objects one at a time.  It's useful in this case to be able to setup observers without having to write an explicit init method.

 

When defining a class or object instance, you can annotate any method to make it an observer by simply adding the observes() helper method.  You must pass one or more property paths to this method, telling SproutCore which properties you want this method to observe:

 

MyApp.preferencesController = SC.Object.create({

 

  showFullName: YES,

  reverseNames: NO,

 

  preferenceDidChange: function(key) {

    console.log('%@ did change!'.fmt(key));

  }.observes('showFullName', 'reverseNames')

 

});

 

MyApp.preferencesController.set('showFullName', NO);

> showFullName did change!

 

MyApp.preferencesController.set('reverseNames', YES);

> reverseNames did change!

 

The above code is exactly equivalent to calling MyApp.preferencesController.addObserver() after the object has been set up, once for 'showFullName' and once for 'reverseNames'. But when you are initializing objects by passing a hash of methods and properties to .create() or .extend(), it is easiest not to have to define an init() method just so that you can call addObserver(). This is where observes() comes into play. It declaratively instructs the constructor to add the corresponding method as an observer.

 

(Which raises an important point to be aware of: observes() doesn't add any observers itself! It merely annotates the method object so that the object constructor knows to add that method as an observer at object setup time. To add an observer after object setup time, do it 'imperatively' using addObserver().)

 

Note also that you can name only one property if you wish and you can name more than two properties.  In fact, you can have a single observer watch as many properties as you like by simply naming them in the observers() helper.

 

Simple Observers 

 

Like the traditional addObserver() method, the observes() helper supports both simple and chained observers.  Because you are statically defining these observers, however, the way you describe chained observers differs slightly.

 

Simple observers are defined just like above - by naming a single property.  Most of the time, you should observe only simple properties like this.  In fact, simple observers are highly optimized in SproutCore. You can have virtually unlimited simple observers on an object and pay almost no performance cost to set them up.

 

Absolute Path Observers

 

Sometimes you may want to observe a property on an object other than the one you defined the observer on.  For example, you might define a contact record that observes the reverseNames property on your preferences.  Since you can only configure your observer by setting a path, the observes() property allows you to name full property paths like so:

 

MyApp.Contact = SC.Object.extend({

 

  reverseNamesDidChange: function() {

    // do something useful

  }.observes('MyApp.preferencesController.reverseNames')

});

 

Unlike addObserver(), simply providing a path like this does not setup a chained observer.  Instead, whenever an instance of Contact is created, SproutCore will walk the property path you named to find an object (MyApp.preferencesController) and a property name (reverseNames), then then bind to it.  The above code, for example, would be equivalent to doing:

 

MyApp.Contact = SC.Object.extend({

 

  reverseNamesDidChange: function() {

    // do something useful

  },

 

  init: function() {

    sc_super();

    MyApp.preferencesController.addObserver('reverseNames', this, this.reverseNamesDidChange);

  }

});

 

Again, note that the addObserver() call above uses a simple property name, it is not a chained observer.  In this example, your observer will fire whenever reverseNames is changed.  It will not fire if you changed MyApp.preferencesController to point to another object.

 

(Frequently-asked question: to observe any change to an ArrayController, or any other array, observe its [] property (e.g. "MyApp.selectedItemsController.[]").  For more, see Runtime-Using SCEnumerable.)

 

Relative Path Observers

 

Often times you will want to observe a property path like the example above, but you only know the object you want to observe relative to the object you are creating.  You can make a property path relative by starting the path with a '.' (dot).  For example, let's say you want to observe the firstName property on the contact again:

 

MyApp.User = SC.Object.extend({

  

  contact: null, // will have a contact;

 

  firstNameDidChange: function() {

    // do something interesting

  }.observes('.contact.firstName'),

 

  init: function() {

    // create a contact to actually observe

    this.set('contact', MyApp.Contact.create());

    

    // activate observing

    sc_super();

  }

});

 

Note that in this example, firstNameDidChange will observe the "firstName" property on whatever contact object happens to be set when the object is initialized.  This is why the code above takes the extra step to make sure a contact record is filled in before property observing is setup in sc_super().

 

Relative property paths are obviously quite limited because they can only observe a static path.  For example, changing the "contact" property would break your code above because your observer won't be changed to track the new contact.  It would continue to track the contact that was present when the User object was first created.

 

On the other hand, because relative paths map to simple property observers underneath, they are also nearly as fast.  You should use relative property paths instead of chained observers whenever you want to refer to a property within your object graph and you are certain that none of the objects in the path other than the final property will change. 

 

Absolute Chained Observers 

 

Since properties paths have a different meaning when using the observes() method, specifying chained observers involves using a special syntax.  Let's say you have an observer like:

 

firstNameDidChange: function() {

 //...

}.observes('MyApp.currentUser.contact.firstName')

 

If you added an observer like this, it would be equivalent to calling:

 

MyApp.currentUser.contact.addObserver('firstName', object, object.firstNameDidChange);

 

SproutCore walks down your property path until it gets to the last dot.  Anything before the last dot is treated as part of the object.  Anything after the last dot is the property name.  To build a chained observer, just replace one of the dots with an asterisk.  SproutCore will stop at the asterisk and treat the rest as a chained observer:

 

 

firstNameDidChange: function() {

 //...

}.observes('MyApp.currentUser*contact.firstName');

 

 

Is the same as:

 

MyApp.currentUser.addObserver('contact.firstName', object, object.firstNameDidChange);

 

Since you have placed more than one property name after the asterisk, this will setup a chained observer, firing anytime either "contact" or "contact.firstName" is changed.

 

Relative Chained Observers

 

Just like you can make paths relative by starting with a period, you can do the same thing with chained observers.  The following would fire the observer anytime this.contact or this.contact.firstName changes:

 

firstNameDidChange: function() {

 //...

}.observes('*contact.firstName');

 

Notifying Observers 

 

Normally SproutCore will automatically handle notifying observers for you whenever you change a property using set():

 

contact.set('firstName', "John");

==> notifies observers of "firstName"

 

Sometimes a property value may change indirectly.  It may be a computed property for example or you might bypass the normal accessor methods for some tightly looped code.  If you every need to manually notify that a property has changed you can use notifyPropertyChange():

 

contact.notifyPropertyChange('firstName')

==> notifies observers of "firstName"

 

notifyPropertyChange() will notify observers that a property has changed even if its value hasn't really changed.  It will also invalidate any caching on the property.  Sometimes this is a good thing, but if used improperly it can lead to major performance problems in your application.

 

Unless you are synthesizing properties or doing some very limited optimizations you generally should not need to call this method.

 

Notifying All Property Changes

 

Sometimes, you may know that several properties have changed on an object but you won't know which ones specifically.  If you determine that it would be more expensive to iterate through and figure out which specific properties have changed than to just notify all the observers and let them sort it out, you can use a "nuclear option" to notify all observers on an object called:

 

contact.allPropertiesDidChange();

==> notifies all observers on any property on the object

 

As you might expect, this method can be very expensive.  You should generally avoid it in your code at all costs.    It is useful to understand this method and how it works however because it will likely affect your app.  

 

The DataStore framework uses this method to notify observers of a Record whenever new data is loaded for that record from the server.  This means that any observers on records in your application may sometimes be triggered even if property values have not actually changed.  Design your observers to exit quickly when they determine that this is the case.  

 

Performance Enhancements

 

KVO can save you lots of code.  It can also make your app very fast or very slow, depending on how you use them.  This section will tell you about some of the performance tricks you can use to make sure your apps remains fast:

 

Avoiding Loops

 

The #1 thing you must do to keep your application fast is to avoid observer loops.  You will often notice these loops right away as a part of your app will suddenly become very slow.  This happens most often when you use notifyPropertyChanges() without actually notifying the change.  The example below shows what a loop might look like:

 

MyApp.Contact = SC.Object.extend({

 

  username: "john",

  email: "john@example.com",

 

  // whenever username changes, compute the user's new email address

  usernameDidChange: function() {

    this.set('email', this.get('username') + '@example.com');

  }.observes('username'),

 

  // whenever email changes, compute user's new username

  emailDidChange: function() {

    var email =this.get('email');

    this.set('username', email.slice(0, email.indexOf('@')));

  }.observes('email')

 

});

 

In this example, changing either username or email will cause an infinite loop because changing one will cause the other to trigger and visa-versa.  Remember that observers fire when you set a property even if you didn't actually change the value.  Observers work this way because testing the value of every set() to determine if it actually changed would be very expensive in some cases.  The situation shown above does not happen often in practice and it is easy to work around so SproutCore expects you to handle this instead of imposing a major performance penalty.

 

To get around this case, you can use a simple helper method called didChangeFor().  This method will store the value of a property and then verify that is has actually changed.  Here is how you would fix the code above:

 

MyApp.Contact = SC.Object.extend({

 

  username: "john",

  email: "john@example.com",

 

  // whenever username changes, compute the user's new email address

  usernameDidChange: function() {

    if (!this.didChangeFor('usernameDidChange', 'username')) return this;

    this.set('email', this.get('username') + '@example.com');

  }.observes('username'),

 

  // whenever email changes, compute user's new username

  emailDidChange: function() {

    if (!this.didChangeFor('emailDidChange', 'email')) return this;

    var email =this.get('email');

    this.set('username', email.slice(0, email.indexOf('@')));

  }.observes('email')

 

}); 

 

The first parameter is a unique key (it can be anything really) that allows didChangeFor() to distinguish between calls from one observer or the other.  After the first parameter, you can pass any number of property names.  didChangeFor() will return YES if any of those properties have changed since the last time you call them.

 

Again, usually loops are not a problem in your application.  If you find yourself getting into observer loops like this frequently it probably means that you are designing your application wrong.  For example, you might be relying too much on observers and not enough on computed properties.

 

If you do find yourself with a loop that you can't design out of your app, didChangeFor() is an efficient way to stop loops dead in their track.

 

Deferring Notifications

 

Property observers fire immediately when you modify a property.  That is, when you call contact.set('firstName', 'foo'), any observers on the "firstName" property are going to fire before the set() method returns. (Chained observers behave a little differently but you can generally assume this is the case.)

 

If you plan to make lots of changes to an object's properties, you can temporarily suspend property notifications by using the beginPropertyChanges() and endPropertyChanges() methods:

 

contact.beginPropertyChanges();

contact.set('firstName', 'John');

contact.set('lastName', 'Doe');

contact.endPropertyChanges();

==> observers for firstName and lastName are fired

 

Any changes you make inside of a beginPropertyChanges()/endPropertyChanges() pairs will not notify observers.  Additionally, you can nest calls to begin/end property changes, allowing you to call other methods that wrap their own calls without worrying about observers firing too early.

 

begin/end property change calls are common in well written applications as they can help focus localized changes into logical batching, matching the structure of your application.  You should consider using these calls if you plan to change more than two properties on a single object and you expect the changes to trigger multiple observers.

 

Suspending Notifications

 

If you plan to make changes to a large number of objects in your application, you can also temporarily suspend all property notifications in your app by using low-level methods on the SC.Observers object:

 

SC.Observers.suspendPropertyObserving();

contact.set('firstName', 'John');

contact.set('lastName', 'Doe');

user.set('contact', contact);

SC.Observers.resumePropertyObserving();

==> observers for contact.firstName, contact.lastName, user.contact are fired

 

Like begin/end property changes, suspend/resume property observing calls can be nested.  Observers will not be notified until the top level resumePropertyObserving() call is issued.

 

You should need to suspend property observing rarely if ever.  Usually if you find yourself using these calls it means something in your application is structured wrong.  Since this code affects notifications for all objects in your application, it can disrupt the normal flow of events, sometimes causing code to break.  Use with care.

 

Checking For Observers

 

Sometimes you might have a property that is expensive to compute or has nasty side effects so you want to call it sparingly.  Most of the time the best way to solve this problem is to use a computed property.  Unlike observers, computed properties only execute when some other part of the application tries to retrieve the property value.  If no one is interested in the property, it never gets computed.  Problem solved.    Additionally, computed properties can take advantage of the built in caching mechanism.

 

If you do have an observer and you want to selectively avoid updating properties, however, you can also check to see if there are currently observers actually listening for the property using hasObserversFor():

 

contact.addObserver('firstName', function() { ... });

contact.hasObserverFor('firstName') ==> YES

contact.hasObserverFor('lastName') ==> NO

 

Debugging Observers

 

For all the benefits KVO can offer, one of its biggest drawbacks is debugging.  Since you write very little glue code and observers are so heavily optimized, it can sometimes be hard to track down exactly what is going on inside of your application.  When you get stuck debugging observer issues, the following tools can help you out.

 

addProbe()

 

If an observer you registered isn't firing, it could mean one of two things:  (1) your observer isn't actually registered or (2) your property is not being changed.  The easiest way to figure this out is to add probes to properties you think should change.  To add a probe just use the addProbe() method:

 

contact.addProbe('firstName');

contact.set('firstName', 'Jane');

> NOTIFY: MyApp.Contact.firstName did change 

 

Add probe simply registers an observer that will fire when the property changes and log some output to the console.  If you add a probe and the property you expect to fire does not actually log output, then the problem is with your property not changing.  If it does log, the problem is in your observer.

 

observersFor()

 

If you ever need to know which observers are actually listening on a property, you can use object.observersFor().  Just pass the property name and this will return a list of any registered observers.

 

IMPORTANT: observersFor() is intended for debugging only.  It is not fast.  You should not use it in application code.

 

 

Moving On 

 

Now that you understand Key Value Coding and Key Value Observing, it's time to move on to the most important feature built on top of these two items - Bindings. Read on!

 

Learn about Bindings »

or Back to Runtime Programming Guide Home »

Comments (2)

Larry Siden said

at 1:02 pm on Dec 28, 2009

observersFor() is really callled observersForKey(), but that's not working either. From Firebug console:

>>> al
EccapCalc.Ledger({guid: assets , account: account-1}) READY_CLEAN _kvo_cloned=Object storeKey=8 store=SC.Store:sc289
>>> al.observersForKey
function()
>>> al.observersForKey('total')
TypeError: type.create is not a function

Hexsprite said

at 7:26 pm on Apr 16, 2010

colincampbell: *owner.content.imageCaption
[10:17pm] colincampbell: * and . do the "same" thing
[10:17pm] colincampbell: * and . will both make the binding relative to that object
[10:17pm] colincampbell: * means that it will fire if anything after it is changed
[10:17pm] colincampbell: so if owner, owner.content, or owner.content.imageCaption is changed, then the binding will get fired
[10:18pm] colincampbell: if you use .owner.content.imageCaption, then the binding will only get fired if owner.content.imageCaption is changed
colincampbell: you can mix too, ie ".owner*content.imageCaption"
colincampbell: but then if owner gets changed, your observer won't fire

You don't have permission to comment on this page.