4

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.

Troels Larsen
  • 3,953
  • 2
  • 33
  • 46

3 Answers3

2

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.

MaxArt
  • 20,295
  • 8
  • 70
  • 78
  • @ExpertSystem You're right, I'll put it between quotation marks :) – MaxArt Jul 31 '14 at 09:47
  • @ExpertSystem Well, I think that depends - is there a way to tell when the O.o callbacks have finished then? Otherwise, it seems quite difficult to solve the OPs question... – Lasse Christiansen Jul 31 '14 at 09:48
  • I feared as much. I agree with the sentiment in the quote, but there are occasions where code _must_ be run immediately, and in order. – Troels Larsen Jul 31 '14 at 09:49
  • @LasseChristiansen-sw_lasse I think it's not solvable with O.o. The only solution is the classic getters/setters approach. But hey, O.o is definitely better than any object polling solution we've used in its stead! – MaxArt Jul 31 '14 at 09:50
  • Well, as @TroelsLarsen mentions, there are cases where code *must* be run immediately, and in order after an object property has changed - so I guess classic getters/setters are the way to go in such cases? – Lasse Christiansen Jul 31 '14 at 09:52
  • @LasseChristiansen-sw_lasse: It does not depend. How could `Object.observe()` be synchronous ? Would you like to have your whole program halted until an observed change happens ? It registers a listener. It is like using `element.addEventListener('click', ...)` and saying "I want `addEventListener` to be synchronous" !?! – gkalpak Jul 31 '14 at 09:53
  • 1
    @ExpertSystem I think he meant that, sometimes, it would be useful if `O.o` was synchronous. But it's a matter of experience, as a synchronous solution would lead to many other potential problems. That's also why we got `MutationObserver` instead of `MutationEvents`. – MaxArt Jul 31 '14 at 09:57
  • Exactly, *sometimes* it would be useful if it was synchronous. And I agree with you @MaxArt - it is definitely a matter of experience, because, as you say, a synchronous solution might cause other problems. – Lasse Christiansen Jul 31 '14 at 09:58
  • @LasseChristiansen-sw_lasse, MaxArt: Seriously, could you explain how an event listener (which is essentially what `O.o` is) could be synchronous (and useful at the same time) ? (BTW, `MutationEvents` are **not** synchronous either !) – gkalpak Jul 31 '14 at 10:08
  • @ExpertSystem: Events in .NET are synchronous, and they are certainly useful. http://stackoverflow.com/questions/7106454/are-c-sharp-events-synchronous – Troels Larsen Jul 31 '14 at 10:24
  • @ExpertSystem You missed my point there: it was about the "notification" system, rather than the classic event calling. By the way, `MutationEvents` are [pretty much synchronous](http://jsbin.com/kojeve/1). `O.o` is nothing like that, it only *resembles* an event listener because of that. – MaxArt Jul 31 '14 at 10:36
  • 1
    @TroelsLarsen, MaxArt: I thought you were talking about the (synchronous/asynchronous) nature of `O.o` itself. Seems like you were talking about the way of invoking the `O.o` listener callback. So, we are aon the same page now and I agree with both :) – gkalpak Jul 31 '14 at 10:48
  • 1
    @ExpertSystem: Ok, I think I understand you now. I guess LasseChristiansen is right. I need to use getters and setters somehow. – Troels Larsen Jul 31 '14 at 10:49
  • 1
    @TroelsLarsen: It depends on what you are trying to do with your code. If you just want to update the view, you don't need getters/setters (thanks to Angular). See, the last example/demo in my answer. – gkalpak Jul 31 '14 at 10:54
  • 1
    @ExpertSystem: Looking back I should never have used the view as an example. This is solely for business logic within the model. So if PropertyA changes, PropertyB gets a new value. – Troels Larsen Jul 31 '14 at 11:00
  • 1
    @TroelsLarsen: In that case (and under the assumption that you need your changes to be propagated synchronously) using a setter seems the best approach. – gkalpak Jul 31 '14 at 11:27
  • 1
    Setters it is then. Thanks everyone! – Troels Larsen Jul 31 '14 at 12:00
  • 1
    @TroelsLarsen You're welcome. It's been a very interesting discussion. – MaxArt Jul 31 '14 at 12:17
1

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.

gkalpak
  • 46,029
  • 8
  • 97
  • 116
  • I abstracted away angular as I (currently) have no need for DependencyInjection in my model. Similarly, I'd prefer not to know anything about angular scopes or similar. I don't mind writing (generating) code for getters and setters. I guess I'll need to find out if Angular can bind to those. – Troels Larsen Jul 31 '14 at 11:23
  • 1
    Angular can bind to anything. But if something happens/changes outside of the Angular context (e.g. in an asynchronously executed callback) you need to explicitly tell Angular something happened, by wrapping the code in `$scope.$apply()` (see my example above). – gkalpak Jul 31 '14 at 12:06
1

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

Marius
  • 983
  • 2
  • 6
  • 16