What does notifier.performChange actually do?

398 views Asked by At

I am trying to understand Object.getNotifier(object).performChange. Conceptually I understand that it is designed for defining "macro" or higher level changes. From the example everyone seems to refer to:

increment: function(amount) {
  var notifier = Object.getNotifier(this);

  notifier.performChange(Thingy.INCREMENT, function() {
    this.a += amount;
    this.b += amount;
  }, this);

  notifier.notify({
    object: this,
    type: Thingy.INCREMENT,
    incremented: amount
  });
}

What I do not understand is, how is this different from simply executing the anonymous function passed to notifier.performChange directly, instead of as a callback? In other words, how does it differ from the below:

increment: function(amount) {
  var notifier = Object.getNotifier(this);

  this.a += amount;
  this.b += amount;

  notifier.notify({
    object: this,
    type: Thingy.INCREMENT,
    incremented: amount
  });
}

I have seen that in the latest spec, notifier.performChange may return an object, which is then issued as a notification, as in:

notifier.performChange(Thing.INCREMENT, function() {
    this.a += amount;
    this.b += amount;

    // a notification is issues with this return value,
    // including the type passed to notifier.performChange,
    // and the object underlying notifier. 
    return {incremented: amount};  
});

That eliminates the need for the following notifier.notify in the original code, but still, is this something other than sugar, or is there a functional difference between this and just making the changes and issuing the notification yourself?

2

There are 2 answers

0
Edwin Reynoso On BEST ANSWER

After an hour of doing a lot of testing I finally figured it out. I had the same question (what is performChange for?), and also the same idea to just take that off and call

this.a += amount;
this.b += amount;

However: the point of notifier.performChange is so that the observer does not observe each change.

I was testing it like this:

var obj = {
  x: 5,
  y: 10
};

function noti() {
  console.log('noti start');
  var notifier = Object.getNotifier(obj);

  notifier.performChange('ok', function() {
    obj.x++;
    obj.y++;
  });

  notifier.notify({
    type: 'ok',
    oldValue: 5
  });
  console.log('noti end');
};

function noti2() {
  console.log('noti2 start');
  var notifier = Object.getNotifier(obj);

  obj.x++;
  obj.y++;

  notifier.notify({
    type: 'ok',
    oldValue: 5
  });
  console.log('noti2 end');
};

function observer(changes) {
  for (var change of changes) {
    console.log('observer: change =', change, ' newValue=', change.object[change.name]);
  }
};

Object.observe(obj, observer, ['ok', 'update']);

console.log('calling noti2()');
noti2(); //will log the changes of update twice becuase of the x and y property of obj

// add delay manually because observer calls are asynchronous and
// we want to clearly separate the notification function calls in our logs
setTimeout(function() {
  console.log('calling noti()');

  noti(); //will only log the ok type. that's what they mean by big change
          //so everything you do inside the performChange won't be observed
}, 100);

It should return the following console output:

calling noti2()
noti2 start
noti2 end
observer: change = Object {type: "update", object: Object, name: "x", oldValue: 5}  newValue= 6
observer: change = Object {type: "update", object: Object, name: "y", oldValue: 10}  newValue= 11
observer: change = Object {object: Object, type: "ok", oldValue: 5}  newValue= undefined

calling noti()
noti start
noti end
observer: change = Object {object: Object, type: "ok", oldValue: 5}  newValue= undefined
3
aaaaaa On

I wanted to provide a definitive answer to this question I was also asking myself, so I had a look at the Object.observe spec.

Here's what you need to know about what Object.getNotifier(obj).performChange(changeType, changeFn) does:

  • It runs changeFn
  • While changeFn is running, it purposely doesn't notify any observer of the changes that may happen to properties of obj
  • You can make changeFn return an object: obj's observers will be notified with that object's own properties

To see for yourself, %NotifierPrototype%.performChange(changeType, changeFn) is what you're looking for in the spec.

Applied to your examples, this means these two lead to exactly the same result, while doing things a little differently:

Example 1:

increment: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT, function() {
        this.a += amount;
        this.b += amount;
    });

    notifier.notify({
        object: this,
        type: Thingy.INCREMENT,
        incremented: amount
    });
}

In this first example:

  • As per performChange()'s behavior, changes to the object's properties inside the callback function will be kept silent
  • Since the callback function returns undefined, performChange() won't notify any observer of anything else
  • However, calling notify() at the end explicitly notifies the appropriate observers with the change record passed

Example 2:

increment: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT, function() {
        this.a += amount;
        this.b += amount;

        return { incremented: amount };  
    });
}

In this second example:

  • As per performChange()'s behavior, changes to the object's properties inside the callback function will be kept silent
  • Since the callback function returns an object, performChange() will notify the appropriate observers with an object that looks the same as the one resulting from explicitly calling notify() in example 1: { object: Object, type: Thingy.INCREMENT, increment: amount }

These two examples should cover most cases where you'd want to use performChange(), and how you can use it. I'm going to keep diving though, for this beast's behavior is quite interesting.


Asynchronicity

Observers are executed asynchronously. That means that everything that happens inside the increment() function in the examples above is actually reported to the observer once increment() has finished executing – and only then.

In other words, all of these:

  • Making a change to the observed object's properties outside of performChange()
  • Returning an object in performChange()'s callback
  • Calling notify()

Will only notify the appropriate observer once increment() has finished running.

Synchronous change delivery

If you need to be aware of the pending changes during increment()'s execution (pending changes = all changes that will be reported to observers at the end of increment(), but haven't yet), there's a solution: Object.deliverChangeRecords(callback).

Be aware though that callback needs to be a reference to a function you've already registered as an observation callback for that object before.

In other words, this won't work:

(function() {
    var obj = { prop: "a" };

    Object.observe(obj, function(changes) {
        console.log(changes);
    });
    
    obj.prop = "b";

    Object.deliverChangeRecords(function(changes) {
        console.log(changes);
    });

    console.log("End of execution");
})(); // Meh, we're notified of changes here, which isn't what we wanted

While this will:

(function() {
    var obj = { prop: "a" },

        callback = function(changes) {
            console.log(changes);
        };
    
    Object.observe(obj, callback)

    obj.prop = "b";

    Object.deliverChangeRecords(callback); // Notified of changes here, synchronously: yay!

    console.log("End of execution");
})();

The reason for this is that internally, calling Object.observe(obj, callback) for an object obj will add the passed callback function to obj's list of observation callbacks (known as [[ChangeObservers]] in the spec). Each of these callbacks will only be executed for specific types of changes (the third Object.observe() argument), or all default ones if no argument is passed. (This is an important detail, since that means that if you want to use a custom type of change, you'll need to explicitly pass it to Object.observe()'s third argument, otherwise you won't be notified of any changes of that type.)

Additionally, every pending change will be added to every matching observation callback's queue internally. That means every observation callback has its own set of pending changes.

And that's exactly what Object.deliverChangeRecords(callback) is for: it takes all pending changes for callback and executes that callback by passing it all these changes.

That explains why deliverChangeRecords() only requires one argument, that of the callback. As illustrated by the example below, passing a callback to deliverChangeRecords() will execute that callback with all its pending changes, including changes from multiple objects. This is in accordance with the general behavior of callbacks, might they be called asynchronously or through deliverChangeRecords().

(function() {
    var obj1 = { prop1: "a" },
        obj2 = { prop2: "a" },

        commonCallback = function(changes) {
            console.log(changes);
        };
    
    Object.observe(obj1, commonCallback);
    Object.observe(obj2, commonCallback);

    obj1.prop1 = "b";
    obj2.prop2 = "b";

    Object.deliverChangeRecords(commonCallback); // Notified of the changes to both obj1.prop1 and obj2.prop2
})();

Also, great usage examples are available in the spec.