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.