Object.observe(..)

One of the holy grails of front-end web development is data binding — listening for updates to a data object and syncing the DOM representation of that data. Most JS frameworks provide some mechanism for these sorts of operations.

It appears likely that post ES6, we’ll see support added directly to the language, via a utility called Object.observe(..). Essentially, the idea is that you can set up a listener to observe an object’s changes, and have a callback called any time a change occurs. You can then update the DOM accordingly, for instance.

There are six types of changes that you can observe:

  • add
  • update
  • delete
  • reconfigure
  • setPrototype
  • preventExtensions

By default, you’ll be notified of all these change types, but you can filter down to only the ones you care about.

Consider:

  1. var obj = { a: 1, b: 2 };
  2. Object.observe(
  3. obj,
  4. function(changes){
  5. for (var change of changes) {
  6. console.log( change );
  7. }
  8. },
  9. [ "add", "update", "delete" ]
  10. );
  11. obj.c = 3;
  12. // { name: "c", object: obj, type: "add" }
  13. obj.a = 42;
  14. // { name: "a", object: obj, type: "update", oldValue: 1 }
  15. delete obj.b;
  16. // { name: "b", object: obj, type: "delete", oldValue: 2 }

In addition to the main "add", "update", and "delete" change types:

  • The "reconfigure" change event is fired if one of the object’s properties is reconfigured with Object.defineProperty(..), such as changing its writable attribute. See the this & Object Prototypes title of this series for more information.
  • The "preventExtensions" change event is fired if the object is made non-extensible via Object.preventExtensions(..).

    Because both Object.seal(..) and Object.freeze(..) also imply Object.preventExtensions(..), they’ll also fire its corresponding change event. In addition, "reconfigure" change events will also be fired for each property on the object.

  • The "setPrototype" change event is fired if the [[Prototype]] of an object is changed, either by setting it with the __proto__ setter, or using Object.setPrototypeOf(..).

Notice that these change events are notified immediately after said change. Don’t confuse this with proxies (see Chapter 7) where you can intercept the actions before they occur. Object observation lets you respond after a change (or set of changes) occurs.

Custom Change Events

In addition to the six built-in change event types, you can also listen for and fire custom change events.

Consider:

  1. function observer(changes){
  2. for (var change of changes) {
  3. if (change.type == "recalc") {
  4. change.object.c =
  5. change.object.oldValue +
  6. change.object.a +
  7. change.object.b;
  8. }
  9. }
  10. }
  11. function changeObj(a,b) {
  12. var notifier = Object.getNotifier( obj );
  13. obj.a = a * 2;
  14. obj.b = b * 3;
  15. // queue up change events into a set
  16. notifier.notify( {
  17. type: "recalc",
  18. name: "c",
  19. oldValue: obj.c
  20. } );
  21. }
  22. var obj = { a: 1, b: 2, c: 3 };
  23. Object.observe(
  24. obj,
  25. observer,
  26. ["recalc"]
  27. );
  28. changeObj( 3, 11 );
  29. obj.a; // 12
  30. obj.b; // 30
  31. obj.c; // 3

The change set ("recalc" custom event) has been queued for delivery to the observer, but not delivered yet, which is why obj.c is still 3.

The changes are by default delivered at the end of the current event loop (see the Async & Performance title of this series). If you want to deliver them immediately, use Object.deliverChangeRecords(observer). Once the change events are delivered, you can observe obj.c updated as expected:

  1. obj.c; // 42

In the previous example, we called notifier.notify(..) with the complete change event record. An alternative form for queuing change records is to use performChange(..), which separates specifying the type of the event from the rest of event record’s properties (via a function callback). Consider:

  1. notifier.performChange( "recalc", function(){
  2. return {
  3. name: "c",
  4. // `this` is the object under observation
  5. oldValue: this.c
  6. };
  7. } );

In certain circumstances, this separation of concerns may map more cleanly to your usage pattern.

Ending Observation

Just like with normal event listeners, you may wish to stop observing an object’s change events. For that, you use Object.unobserve(..).

For example:

  1. var obj = { a: 1, b: 2 };
  2. Object.observe( obj, function observer(changes) {
  3. for (var change of changes) {
  4. if (change.type == "setPrototype") {
  5. Object.unobserve(
  6. change.object, observer
  7. );
  8. break;
  9. }
  10. }
  11. } );

In this trivial example, we listen for change events until we see the "setPrototype" event come through, at which time we stop observing any more change events.