最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - how to know which object property changed? - Stack Overflow

programmeradmin4浏览0评论

I have a map of 10 objects. Each object has 10 properties val1, ..., val10. Now to check of a specific property of a specific object in the map has changed, I would need to write $scope.$watch 100 times. How to watch an entire map/JSobject and know exactly which property has changed?

/*watch individual property*/
            $scope.$watch('map[someid].val1', function(new_val, old_val)
            {
                //do something
            });

    /*watch entire object, how would this help me?*/
            $scope.$watch('map', function(new_val, old_val)
            {
                //do something
            }, true);

I have a map of 10 objects. Each object has 10 properties val1, ..., val10. Now to check of a specific property of a specific object in the map has changed, I would need to write $scope.$watch 100 times. How to watch an entire map/JSobject and know exactly which property has changed?

/*watch individual property*/
            $scope.$watch('map[someid].val1', function(new_val, old_val)
            {
                //do something
            });

    /*watch entire object, how would this help me?*/
            $scope.$watch('map', function(new_val, old_val)
            {
                //do something
            }, true);
Share Improve this question asked Apr 1, 2014 at 15:13 user494461user494461 3
  • I would suggest some Aspect Oriented Programming (AOP) library: stackoverflow./questions/1005486/javascript-aop-libraries – the_marcelo_r Commented Apr 1, 2014 at 15:19
  • 2 Try watchCollection. docs.angularjs/api/ng/type/$rootScope.Scope – mainguy Commented Apr 1, 2014 at 15:20
  • This is an interesting question. Please post the solution once you find it. It would be great. Thanks! – pj013 Commented Apr 1, 2014 at 15:23
Add a ment  | 

6 Answers 6

Reset to default 5 +25

As the previous answers seem to suggest that AngularJS doesn't provide a targeted solution for your problem, let me propose one using vanilla JS.

AFAIK if you want implement a watcher of an object, you'll need a getter/setter pattern (aka. Mutator method) of some sort. I don't know anything about AngularJS, but I'm quite certain that they must be using something similar as well. There are multiple ways to implement getters and setters in JavaScript (see the Wikipedia page linked above), but in general you have three main choices:

  • To use an external handler (an object which hides the internal data structure pletely, and only provides an API for reading and writing)
  • To use Java-style getSomething and setSomething methods or
  • To use native JavaScript property getters and setters introduced in ECMAScript 5.1

For some reason I've seen many more examples of doing it via the first two methods, but on the other hand I've seen one example of a quite plex and beautiful API built with the third method. I'll give you an example of solving your problem via the third method, using Object.defineProperty:

The idea is to provide a watcher function, which is then called as a part of a property setter:

function watcher(objectIndex, attributeName) {
  console.log('map[' + objectIndex + '].' + attributeName + ' has changed');
}

Object.defineProperty is a little bit like a plex way to define property for an object. So when you would normally write

obj.val1 = "foo";

you'll write

Object.defineProperty(obj, 'val1', {
  get: function ()         { /* ... */ },
  set: function (newValue) { /* ... */ },
  // possible other parameters for defineProperty
})

Ie. you define what you want to be done when the obj.val1 is either read and written. This gives you a possibility to also call your watcher function as a part of the setter:

Object.defineProperty(obj, 'val1', {
  get: function ()         { /* ... */ },
  set: function (newValue) {
    // set the value somewhere; just don't use this.val1
    /* call the */ watcher(/* with args, of course */)
  }
})

It may seem like it's quite a many lines of code for quite a simple thing, but you can use functional programming techniques and closures to structure your program so that it isn't actually so plicated to write. So in essence even though this may not seem like an "AngularJS-way" of doing things, it isn't actually too much overhead to implement by hand.

Here's a full code example targeting your problem: http://codepen.io/cido/pen/DEpLj/

In the example the watcher function is quite hardly coded to be a part of the data structure implementation. However if you need to re-use this code in multiple places, nothing really stops you from extending the solution with eg. addEventListener-stylish hooks or etc.

As this approach uses closures quite much, it may have some memory implications, if you're really handling massive amounts of data. However, in most use cases you don't have to worry about that kind of things. And on the other hand, all solutions based on "deep equality" checks aren't going to be super memory-efficient or fast ones either.

Currently there is nothing in AngularJS to handle this for you.

You will either have to implement it yourself or find a library that does it for you.

If you want to implement it yourself using any of the built-in watcher functions there are three options:

1. $watch with false as third parameter: Comparing for reference equality. Not an option in your case.

2. $watchCollection: Will shallow watch the ten objects in your map. Not an option.

3. $watch with true as a third parameter: Will pare for object equality using angular.equals. Will work for your use case.

Below is an example that registers a watcher for the entire map with true as the third parameter, and does the parison manually.

Note: Depending on your application (number of bindings and calls to $digest for example) watching large plex objects may have adverse memory and performance implications. As this example uses another angular.copy ($watch uses at least one internally) it will impact performance even more.

$scope.map = {};

for (var i = 0; i < 3; i++) {
  $scope.map[i] = {
    property1: 'value1',
    property2: 'value2',
    property3: 'value3'
  };
}

var source = [];

var updateSource = function(newSource) {
  angular.copy(newSource, source);
};

updateSource($scope.map);

$scope.$watch("map", function(newMap, previousMap) {

  if (newMap !== previousMap) {

    for (var id in newMap) {

      if (angular.equals(newMap[id], source[id])) continue;

      var newObject = newMap[id];
      var previousObject = source[id];

      for (var property in newObject) {

        if (!newObject.hasOwnProperty(property) ||
          angular.equals(newObject[property], previousObject[property])) continue;

        var cd = $scope.changeDetection = {};

        cd.changedObjectId = id;
        cd.previousObject = previousObject;
        cd.newObject = newObject;
        cd.changedProperty = property;
        cd.previousPropertyValue = previousObject[property];
        cd.newPropertyValue = newObject[property];
      }

      updateSource(newMap);
    }
  }
}, true);

Demo: http://plnkr.co/edit/MtfJTY3D4osUtXtIcCiR?p=preview

I would try using this from the angular docs

$scope.$watchCollection(collection, function(newValue, oldValue) {
 //do stuff when collection changes
});

http://docs.angularjs/api/ng/type/$rootScope.Scope

$watch has a not so known third parameter, which if true, will use angular.equals() instead of using === to test for equality. Now angular.equals() does consider two object equals if any of a set of conditions is true, one of them being:

  • Both objects or values are of the same type and all of their properties are equal by paring them with angular.equals.

This is basically saying it will do a recursive (deep) parison, which is what you need. So detecting a change in any nested property is as easy as:

scope.$watch('map', function (newmap,oldmap) { 
  // detect what changed manually with your own recursive function
}, true);

This won't tell you what exactly changed, so you will need to do that part yourself. Consider binding your view directly to map instead and make sure all changes happen inside the angular $digest cycle or have them happen wrapped in $apply for a much easier approach to the problem. If for your use case it is imperative that you use a $watch you can also recursively iterate the objects and set up individual watches in there.

I know I'm late to the party, but I needed something similar that the above answers didn't help.

I was using Angular's $watch function to detect changes in a variable. Not only did I need to know whether a property had changed on the variable, but I also wanted to make sure that the property that changed was not a temporary, calculated field. In other words, I wanted to ignore certain properties.

Here's the code: https://jsfiddle/rv01x6jo/

Here's how to use it:

$scope.$watch($scope.MyObjectToWatch, function(newValue, oldValue) {

    // To only return the difference
    var difference = diff(newValue, oldValue);  

    // To exclude certain properties from being evaluated (i.e. temporary/calculated/meta properties)
    var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

    if (Object.keys(difference).length == 0) 
        return;  // Nothing has changed
    else
        doSomething();  // Something has changed

});

By examining the Object.keys(difference) array, you can see all of the properties that have changed.

Hope this helps someone.

One way is to write a small function that flattens the objects now and then and forms strings. From that point, its pretty simple to pare the strings and find the point of mismatch, which is the point where the object has changed.

Needless to say, all of the object properties should be exposed.

var flatten = function (key, obj) {
    var ret = '';
    $.each(obj, function (_key, val) {
        if (typeof val == 'object') {
            ret += flatten(key + '.' + _key, val);
        } else {
            ret += ((key ? key + '.' : '') + _key + ':' + val + ',');
        }
    });
    return ret;
};

Fiddle

Followed by something like:

$scope.$watch(obj, function (now, then) {
    var nowFlat = flatten(now),
        thenFlat = flatten(then);
    pareStrings(nowFlat, thenFlat); // add custom string pare function here
}, true)

As a performance improvement, you can always cache nowFlat and use it as thenFlat when the $watch is triggered next.

发布评论

评论列表(0)

  1. 暂无评论