1

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.

Bergi
  • 513,640
  • 108
  • 821
  • 1,164
CodeManX
  • 9,290
  • 3
  • 41
  • 58

1 Answers1

6

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.

  • Good point, but how does `Object.observe()` behave in the exact situation you described? If two other objects contain the first object, will 3 change events occur? The new object is passed to the callback, so inheritance / object nesting wouldn't be a problem. BTW: The `path` shouldn't contain `movall`, `ob` or `ob2`, but only what follows. In combination with the passed object, the change `type` and `oldValue`, it would be possible to directly create an OT operation. – CodeManX Nov 06 '14 at 16:36
  • Change events "occur" only if they observed, like a tree falling in the forest. So if we also observed `ob2`, then yes, another change event would be sent to that observer. –  Nov 06 '14 at 16:40
  • Just to clarify: If we observe `movall` and `ob2`, `ob2` contains `movall`, and I make a change to `ob2` — `ob2.b.movall.moo = "foo"` — will there be one change event to `movall` and one to `ob2`? If yes, then there's no path ambiguity, as it would be relative to the object we observe, regardless of nested objects (`moo` for `movall` and `b.movall.moo` for `ob2`). – CodeManX Nov 06 '14 at 16:53
  • 2
    If I understand you comment correctly, then without additional machinery, a change to `ob2.movall.moo` is **not** a change to `ob2`. Changes are reported only on the object containing the property. A change to `ob2.movall.moo` is **not** a change on any property of `ob2`, it's a change to the **internals** of the value of one of its properties. –  Nov 06 '14 at 16:58
  • I see, and after same though it makes sense and I don't need any additional machinery to get events for `ob2` in this case. However, I don't understand how there could possibly a path ambiguity in the change event to `movall`'s observer callback then. – CodeManX Nov 06 '14 at 17:04
  • Your code doesn't work in Chrome Canary, CPU will go to full load and memory is eaten up increasingly (up to gigabytes) - an infinite recursion / one event causing another in a ping-pong manor? (both with and without harmony flag). Firefox + polyfill doesn't do anything, not even give an error. – CodeManX Nov 06 '14 at 18:23
  • Sorry, take a look now. It was infinite-looping on the `modify` event. –  Nov 06 '14 at 19:34
  • Thanks, it's basically working now. A few remarks: non-atomic types inside arrays are not observed (array inside array, object inside array), I assume arrays need to be handled differently (you currently ignore them). Only paths which existed before the call to `notifySubobjectChanges()` are observed, if properties are added later, one would either need to re-run that function and add a safety check to not observe the same object a second time or automatically observe new properties as they are added (catch in the already existing observation handler on "add" event). – CodeManX Nov 06 '14 at 22:15
  • @CoDEmanX, thanks for the feedback. Not quite sure what the problems would be with arrays, since they are just objects with properties, unfortunately don't have time to look at that now. Yes, you would need additional logic to handle added properties, which wouldn't necessarily be that simple. –  Nov 07 '14 at 02:39