The new Object.observe() and some pitfalls

Front End Javascript

There is more Harmony inside of Chrome, since they release Chrome 36 stable a few days ago. Object.observe, a specification to observe property changes in Objects and Arrays, is awaiting final approval by the ECMA TC39 guys and there are already tons of blog posts about how to use the new functionality.
A lot of people are very excited about this new API and there are also statements flying around that this will make two-way data binding frameworks unnecessary.

This blog post will not cover all functionalities of the new specification but focus on some pitfalls that I’ve discovered while using the new API. Let’s start with a simple example of how to use Object.observe:

 

 

 

var object = {};
Object.observe(object, function(){
  console.log(object.name);
});
 
object.name = 'Test';

The above example is adding an observer to a simple object. If we add, delete or modify a property of this object after we have added the observer, the observer will execute the callback function that was registered.
In our example setting object.name will trigger the observer callback and we are logging the new value to the console.

Let’s observe a bit closer what’s happening under the hood. We are registering an observer and then add a new property to the object that is observed. This is all happening in the main thread of javascript. After setting object.name, the observer will trigger a system event that invokes our callback function. It’s important to understand that the observer does not block execution of the main thread when a property gets changes but adds an event to the javascript message queue. Later on we’ll see some pitfalls you need to watch out for as a result of this.

If we want to change our example above to be more generic and do different operations on the object properties we could do so with the following code:

var object = {};
Object.observe(object, function(changeRecords){
  changeRecords.forEach(function(changeRecord) {
    console.log(changeRecord);
  });
});
 
object.p1 = 'Property 1';
object.p2 = 'Property 2';
object.p2 = 'Property II';
delete object.p1;

In this example you can see that the callback function accepts a parameter changeRecords. This parameter will represent an array of changes that occurred to the object. A change record is an object with the following format:

 

var changeRecord = {
  object: {...}, 
  type: 'update', 
  name: 'p2', 
  oldValue: 'Property 2'
}

The object property will always be set to the object that is observed. The type property is the notification type and in our example we have two add change records as well as one update and one deletechange. The name property is containing the name of the object property that was changed. If the change record is an update than there is an additional property oldValue that contains the value before the update.

Now lets take a look at an example where we can outline two common pitfalls and put the nature of Object.observe to the test. The following example is a nonsensical implementation of immutable object properties using the new Observe API.

(function(){
  'use strict';
 
  var object = {
    immutables: ['id', 'name']
  };
   
  function immutable(changeRecords) {
    changeRecords.forEach(function(change) {
 
      if(change.type === 'update' &&
         change.object.immutables.indexOf(change.name) !== -1) {
         
        Object.unobserve(change.object, immutable);
        change.object[change.name] = change.oldValue;
        Object.observe(change.object, immutable);
      }
    });
  }
 
  Object.observe(object, immutable);
 
  object.name = 'Test';
 
  console.log(object.name);
   
  object.name = 'Updated test';
   
  console.log(object.name);
   
  setTimeout(function() {
     console.log(object.name);
  }, 0);
   
}());

This might look a little weird to you but what we are trying to do here is adding an observer callback that listens for property value updates and if it finds an update it will change the value back to the old value. It uses the object.immutables array to check if the updated property should be treated as an immutable property.
What you can also see is that there is a call to Object.unobserve andObject.observe surrounding the code where we change the updated value back to the old value. This is required because if we would still observe the object while doing this we would trigger an other change event, that triggers an other change event, that triggers an other change event… you get the point. If you’re planning to modify object properties of an observed object in the observer itself (which is probably something you should never do) you need to be cautious to not pump an infinite amount of events into the event queue and cause the browser to die.

Let’s look at the second and probably most annoying pitfall when using Object.observe. Because of the event driven nature of the Observe API we can not expect changes applied in observer callbacks to be available immediately nor can we await the result as it’s created by a simple object modification. Now we can also tell why the output of our example is “Test”, “Update test”, “Test”. I’d expect from my immutable objects to prevent any updates and that the second output would also be “Test” but it’s “Update test”. The reason for this is that our immutable observer is actually changing the value back to the original value through a callback that is in the message queue and the event loop needs to pick it up first. As we are running in our function frame this is not happening before the console.log. However, if we use setTimeout with 0 delay, which is effectively just adding an other function into the message queue, we can ensure that this is running after the immutable observer callback and prints the expected value.

This problem is still not so obvious and also nobody actually needs immutable object properties in javascript. But there are more obvious cases where it can get really nasty.

Let’s assume you would like to build a two-way data binding framework using Object.observe. One central feature would be that you can observe complex object structures that contain nested objects and hierarchies. Object.observe does not support recursive object observation by default so you’d need to do something like this:

(function(){
  'use strict';
 
  var object = {};
   
  function recursiveObserve(object, callback) {
    Object.observe(object, function(changeRecords) {
      changeRecords.forEach(function(change) {
        if(['add', 'update'].indexOf(change.type) !== -1 && 
           typeof change.object[change.name] === 'object') {
          recursiveObserve(change.object[change.name], callback);
        }
         
        callback(change);
      });
     
    });
  }
   
  recursiveObserve(object, function(change) {
    console.log([change.type, 
                 change.name, 
                 change.object[change.name]].join(' '));
  });
 
  object.level1 = {
    level2: {
      name: 'level2'
    }
  };
   
  object.level1.level2.name = 'Hello';
  
}());

In the above example we are setting up a recursive observer on an empty object. The observer callback is checking for properties that get added or updated. If the new value is an object then we call our recursiveObserver function on the new object. This will recursively make sure that any new objects that find their way into the root object will also be observed. This would be very nice in a regular call stack but as this all happens in the message queue we have a problem. The callback function we can pass to our recursiveObserve function, logs all changes to observed objects. But in our example the output is just “add level1 [object Object]” and not “add level1 [object Object]” followed by “update name Hello” as we would have expected. The problem is that for the added level1 object, the observer is not setup yet when we update the name property of level1.level2.

This behavior becomes very annoying when we’d like to observe changes in nested objects. This is also a very common use-case in data binding and synchronization, so we cannot just rely on flat object hierarchies which would be a stupid restriction.

The first thing that comes to our mind when we think about synchronizing “asynchronous” behavior is probably the concept of callbacks, Promises or if we’d have ECMA6 ready maybe generator functions. However we face the problem that the object property observer operates on native level and we have no other option than the observer callback itself to intercept a simple object property change. For a Promise we would need to have control over the point where we want to start the contract of the Promise. However, we don’t. We just set a property of an object myObject.name = ‘Test’. There is no way to create a promise and resolve it when the observer kicks in. One workaround would be to use a wrapper / helper where we announce a property change first (something likeMyObserver.set(myObject.level1, ‘level2’, {name: ‘test’}) but if we would need to do this, then what’s the point of using observers? We could just use an Ember.js like Model object.

Up to this point I couldn’t come up with a proper solution for this problem and I don’t know anyone who does. If you have a solution for this problem please share it with me because I’m really tearing my hair out on this.

For the moment the best solution I can come up with is by using wrapper functions that you can use to add or update new objects into existing ones. The wrapper function is recursively adding observers for all properties that are objects and then returns the object itself so you can easily wrap the function around your assignments.

(function(){
  'use strict';
 
  var object = {};
   
  function observe(object, callback) {
    if(typeof object === 'object') {
      Object.observe(object, function(changeRecord) {
        if(callback) {
          changeRecord.forEach(callback);
        }
      });
       
      Object.keys(object).forEach(function(property) {
        observe(object[property], callback);
      });
    }
     
    return object;
  }
   
  observe(object, function(change) {
    logChange(change);
  });
   
  function logChange(change) {
    console.log([
      change.type,
      change.name,
      change.object[change.name]
    ].join(' '));
  }
 
  object.level1 = observe({
    level2: {
      name: 'level2'
    }
  }, function(change) {
    logChange(change);
  });
   
  object.level1.level2 = observe({
    name: 'new level2'
  }, function(change) {
    logChange(change);
  });
   
  object.level1.level2.name = 'most recent level2';
   
  object.complex = observe({
    persons: [
      {
        name: 'David Hasslehof'
      },
      {
        name: 'Peter Griffin'
      }
    ]
  }, function(change) {
    logChange(change);
  });
   
  object.complex.persons[0].name = 'Stan Marsh';
  
}());

It’s a rather weak solution and if you’d pack this into a framework it would not be so convenient for the users to make changes to objects. I’ve also though about using Object.deliverChangeRecords as you can see in this ECMA example, but I guess it will not solve the problem either.
As already stated, if you can come up with a better solutions please post a comment and let me know.

No Thoughts to The new Object.observe() and some pitfalls

Comments are closed.