I have been experimenting with Object.observe in Chrome v36. My original intent was to use this for business logic in my models, but the asynchronous behaviour seems to make this impossible. I have boiled this down to the following example:
function Person(name) {
this.name = name;
this.someOtherProperty = null;
this.watch = function()
{
var self = this;
Object.observe(this, function(changes){
for(var i = 0; i < changes.length; i++)
{
if(changes[i].name == "name")
{
self.someOtherProperty = changes[i].newValue;
console.log("Property Changed");
}
}
});
}
}
$(function () {
var p = new Person("Alice");
p.watch();
p.name = "Bob";
$("#output").text("Output: "+ p.someOtherProperty);
console.log("Output");
});
JSFiddle link, with jQuery.
My issue is that "Output" is called before "Property Changed". Is there any way to make Object.Observe synchronous, or should I be doing this a better way? (I am using AngularJS, btw.)
The issue here is not adding text to the DOM, or outputting to the console. My business logic requires me to immediately update someOtherPropety
when name
changes, and I'd prefer encapsulating this logic in my model.
Obviously, this is just an example case, but I have business rules that rely on being executed instantly.
Object.observe
, "sadly" (read next), doesn't perform a synchronous task. It sends notifications of the changes as soon as a "micro-task" ends.
This is explained here.
Years of experience on the web platform have taught us that a synchronous approach is the first thing you try because its the easiest to wrap your head around. The problem is it creates a fundamentally dangerous processing model. If you're writing code and say, update the property of an object, you don't really want a situation having update the property of that object could have invited some arbitrary code to go do whatever it wanted. It's not ideal to have your assumptions invalidated as you're running through the middle of a function.
So, your "micro-task" ends after console.log("Output")
has been called, then Object.observe
notifies the changes on the object.
The classic method to have synchronous events is using getters and setters instead:
Person.prototype.setName = function(name) {
this.name = name;
console.log("Name Changed");
};
p.setName("Bob");
Of course, that would force you to create getters and setters for every property you want to watch, and forget about events on deleting and adding new properties.
As you say, observe is not synchronous. But you could maybe make watch take a callback and do your update of "someOtherProperty" there. Like this
$(function () {
var p = new Person("Alice");
p.watch(function(){
$("#output").text("Output: "+ p.someOtherProperty);
});
p.name = "Bob";
console.log("Output");
});
Updated jsfiddle
It doesn't make sense to have Object.observe
behave synchronously. It would have to block the thread and wait until something changes.
You should pass a callback to your watch
function to be executed whenever something changes:
this.watch = function (callback) {
var self = this;
Object.observe(this, function (changes) {
changes.forEach(function (change) {
if (change.name === 'name') {
self.someOtherProperty = change.newValue;
console.log("Property Changed");
callback();
}
});
});
}
$(function () {
var p = new Person("Alice");
p.watch(function () {
// !!!!! OF COURCE YOU SHOULD NEVER DO IT LIKE THIS IN ANGULAR !!!! //
$("#output").text("Output: " + p.someOtherProperty);
console.log("Output");
});
p.name = "Bob";
});
BTW, if you are using Angular (which by your code and fiddle is not at all obvious) you shouldn't care about executing any code when an obseved change happens. As long as you wrapped the code in $scope.$apply()
Angular would take care of updating the view etc.
E.g.:
<div ng-controller="someCtrl">
Output: {{p.someOtherProperty}}
</div>
.controller('someCtrl', function ($scope, Person) {
$scope.p = new Person('Alice');
$scope.p.watch();
$scope.p.name = 'Bob';
});
app.factory('Person', function ($rootScope) {
return function Person(name) {
var self = this;
self.name = name;
self.someOtherProperty = null;
this.watch = function () {
Object.observe(self, function (changes) {
$rootScope.$apply(function () {
changes.forEach(function (change) {
console.log(change);
if (change.name === 'name') {
self.someOtherProperty = self.name;
}
});
});
});
};
};
});
See, also, this short Angular demo.
Better yet, see this more "real-worldy" demo.
Basically, the advantage of using O.o
instead of Angular's dirty checking is that you save on $$watchers
and thus your $digest
cycles are faster and less expensive.
Angular will also use this mechanism (O.o
) anyway when ES6 comes out.