Search code examples
javascriptobserversecmascript-harmonyobject.observe

Why does Object.observe() not provide the data path of change to a callback?


The changes array of an Object.observe() callback contains objects with the following four properties:

  • name
  • object
  • type
  • oldValue

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe#Parameters

Why isn't there a path provided natively? Example:

var ob = {
    foo: [
        {moo: "bar", val: 5},
        {val: 8}
    ]
}

ob.foo[0].val = 1;
// callback should provide path "foo.0.val" or "foo[0].val"

There's a Node.js module that extends Object.observe() to also include the path: observed.js,
but I worry the performance gain of a native observe() will be lost (if no, could you please explain how it is implemented then?). It might be possible to browserify the module, but can't imagine it will perform well in a synchronous environment and I still wonder why nobody seems to have thought about an additional path property.


Solution

  • Because there is no clear path.

    Consider the following:

    var movall = {moo: "bar", val: 5};
    var ob1    = {a: mooval};
    var ob2    = {b: movall};
    

    Now let's say I observe movall. Then I update moo. What is the path? Is it movall.moo, or ob1.a.moo, or ob2.b.moo? If I observe ob1, there is no change reported, since there is no change to any of its properties (the change was internal to one of its properties, which doesn't count).

    Objects are independent of their existence nested within other objects. They can be nested within multiple other objects. There is no unique "path" that describes how to get from potentially multiple starting points down to a specific property which may have changed.

    Nor does JS know the path by which you reached the property being changed. So in ob.foo[0].val = 1;, JS simply evaluates the chain, arrives at the foo[0] object, changes its val property, and at that point has no idea how it happened to arrive at foo[0]. All it knows is that foo[0] has changed. It changed within ob, but it might also have changed within some other object that happens to have foo[0] as a property.

    However, you can possibly achieve what you seem to be trying to by building some machinery on top of the low-level observe/notify mechanism. We shall define a function on an object which sets up observers on its property objects, and so on recursively, and propagates change records back up with properly constructed paths:

    function notifySubobjectChanges(object) {
      var notifier = Object.getNotifier(object);        // get notifier for this object
      for (var k in object) {                           // loop over its properties
        var prop = object[k];                           // get property value
        if (!prop || typeof prop !== 'object') break;   // skip over non-objects
        Object.observe(prop, function(changes) {        // observe the property value
          changes.forEach(function(change) {            // and for each change
            notifier.notify({                           // notify parent object
              object: change.object,                    // with a modified changerec
              name: change.name,                        // which is basically the same
              type: change.type, 
              oldValue: change.oldValue, 
              path: k + 
                (change.path ? '.' + change.path : '')  // but has an addt'l path property
            });
          });
        });
        notifySubobjectChanges(prop);                   // repeat for sub-subproperties
      }
    }
    

    (Note: the change object is frozen and we cannot add anything to it, so we have to copy it.)

    Now

    a = { a: { b: {c: 1 } } };                     // nested objects
    notifySubobjectChanges(a);                     // set up recursive observers
    Object.observe(a, console.log.bind(console));  // log changes to console
    a.a.b.c = 99;
    
    >> 0: Object
      name: "c"
      object: Object
      oldValue: 1
      path: "a.b"                                  // <=== here is your path!
      type: "update"
    

    The above code is not production-quality, use at your own risk.