ChatGPT解决这个技术问题 Extra ChatGPT

Watch multiple $scope attributes

Is there a way to subscribe to events on multiple objects using $watch

E.g.

$scope.$watch('item1, item2', function () { });

P
Paolo Moretti

Starting from AngularJS 1.3 there's a new method called $watchGroup for observing a set of expressions.

$scope.foo = 'foo';
$scope.bar = 'bar';

$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
  // newValues array contains the current values of the watch expressions
  // with the indexes matching those of the watchExpression array
  // i.e.
  // newValues[0] -> $scope.foo 
  // and 
  // newValues[1] -> $scope.bar 
});

what's the difference against $watchCollection('[a, b]') ??
The difference is that you can use a proper array instead of a string that looks like an array
I had a similar problem but using controllerAs pattern. In my case the fix consisted in prepend to the variables the name of the controller: $scope.$watchGroup(['mycustomctrl.foo', 'mycustomctrl.bar'],...
This doesn't seem to work for me on Angular 1.6. If I put a console log on the function, I can see that it just runs once with newValues and oldValues as undefined, while individually watching the properties work as usual.
R
Răzvan Flavius Panda

Beginning with AngularJS 1.1.4 you can use $watchCollection:

$scope.$watchCollection('[item1, item2]', function(newValues, oldValues){
    // do stuff here
    // newValues and oldValues contain the new and respectively old value
    // of the observed collection array
});

Plunker example here

Documentation here


Note that the first parameter is not an array. It's a string that looks like an array.
thanks for this. if it works for me, I will give you a thousand gold coins - virtual ones, of course :)
To be clear (took me a few minutes to realize) $watchCollection is intended to be handed an object and provide a watch for changes to any of the object's properties, whereas $watchGroup is intended to be handed an array of individual properties to watch for changes to. Slightly different to tackle similar yet different problems. Phew!
Somehow, newValues and oldValues are always equal when you use this method: plnkr.co/edit/SwwdpQcNf78pQ80hhXMr
Thanks. It solved my problem. Is there any performance hit/issue of using $watch?
A
Andrew Joslin

$watch first parameter can also be a function.

$scope.$watch(function watchBothItems() {
  return itemsCombinedValue();
}, function whenItemsChange() {
  //stuff
});

If your two combined values are simple, the first parameter is just an angular expression normally. For example, firstName and lastName:

$scope.$watch('firstName + lastName', function() {
  //stuff
});

This seems like a use-case that's crying out for inclusion in the framework by default. Even a computed property as simple as fullName = firstName + " " + lastName requires passing a function as the first argument (rather than a simple expression like 'firstName, lastName'). Angular is SO powerful, but there are some areas where it can be enormously more developer-friendly in ways that don't (seem to) compromise performance or design principles.
Well $watch just takes an angular expression, which is evaluated on the scope it's called on. So you could do $scope.$watch('firstName + lastName', function() {...}). You could also just do two watches: function nameChanged() {}; $scope.$watch('firstName', nameChanged); $scope.$watch('lastName', nameChanged);. I added the simple case to the answer.
The plus in 'firstName + lastName' is string concatenation. So angular just checks whether the result of the concatenation is different now.
String concatenation requires a little care - if you change "paran orman" to "para norman" than you won't trigger a response; best to put a divider like "\t|\t" between the first and second term, or just stick to JSON which is definitive
although amberjs sucks, but it has this feature build in! hear that angular guys. I guess doing to watches is the most rational doable way.
K
Karen Zilles

Here's a solution very similar to your original pseudo-code that actually works:

$scope.$watch('[item1, item2] | json', function () { });

EDIT: Okay, I think this is even better:

$scope.$watch('[item1, item2]', function () { }, true);

Basically we're skipping the json step, which seemed dumb to begin with, but it wasn't working without it. They key is the often omitted 3rd parameter which turns on object equality as opposed to reference equality. Then the comparisons between our created array objects actually work right.


I had a similar question. And this is the correct answer for me.
Both have a slight performance overhead if the items in the array expression aren't simple types.
That is true.. it's doing a full depth comparison of the component objects. Which may or may not be what you want. See the currently approved answer for a version using modern angular that doesn't do a full depth comparison.
For some reason, when I was trying to use the $watchCollection over an array, I got this error TypeError: Object #<Object> has no method '$watchCollection' but this solution helps me to solve my problem !
Cool, you can even watch values within objects: $scope.$watch('[chaptercontent.Id, anchorid]', function (newValues, oldValues) { ... }, true);
Y
Yang Zhang

You can use functions in $watchGroup to select fields of an object in scope.

        $scope.$watchGroup(
        [function () { return _this.$scope.ViewModel.Monitor1Scale; },   
         function () { return _this.$scope.ViewModel.Monitor2Scale; }],  
         function (newVal, oldVal, scope) 
         {
             if (newVal != oldVal) {
                 _this.updateMonitorScales();
             }
         });

Why do you use _this? Doesn't the scope get carried down into the functions without explicitly defining it?
I use typescript, the presented code is compiled result.
A
Adrian Mole

Why not simply wrap it in a forEach?

angular.forEach(['a', 'b', 'c'], function (key) {
  scope.$watch(key, function (v) {
    changed();
  });
});

It's about the same overhead as providing a function for the combined value, without actually having to worry about the value composition.


In this case log() would be executed several times, right? I think in case of nested $watch functions it wouldn't. And it should not in the solution for watching several attributes at once.
No, log is only executed once per change per watched property. Why would it be invoked multiple times?
Right, I mean it wouldn't work nice if we want to watch all of them simultaneously.
Sure why not? I do it that way in a project of mine. changed() is invoked whenever something changes in either of the properties. That's the exact same behavior as if a function for the combined value was provided.
It's slightly different in that if multiple of the items in the array change at the same time the $watch will only fire the handler once instead of once per item changed.
J
Jay

A slightly safer solution to combine values might be to use the following as your $watch function:

function() { return angular.toJson([item1, item2]) }

or

$scope.$watch(
  function() {
    return angular.toJson([item1, item2]);
  },
  function() {
    // Stuff to do after either value changes
  });

A
Artem Andreev

$watch first parameter can be angular expression or function. See documentation on $scope.$watch. It contains a lot of useful info about how $watch method works: when watchExpression is called, how angular compares results, etc.


v
vinculis

how about:

scope.$watch(function() { 
   return { 
      a: thing-one, 
      b: thing-two, 
      c: red-fish, 
      d: blue-fish 
   }; 
}, listener...);

Without the third true parameter to to scope.$watch (as in your example), listener() would fire every single time, because the anonymous hash has a different reference every time.
I found this solution worked for my situation. For me, it was more like: return { a: functionA(), b: functionB(), ... }. I then check the returned objects of the new and old values against each other.
A
Akash Shinde
$scope.$watch('age + name', function () {
  //called when name or age changed
});

Here function will get called when both age and name value get changed.


Also be sure in this case not use the newValue and oldValue arguments that angular passes into the callback because they will be the resulting concatenation and not just the field that was changed
V
Vinit Solanki

Angular introduced $watchGroup in version 1.3 using which we can watch multiple variables, with a single $watchGroup block $watchGroup takes array as first parameter in which we can include all of our variables to watch.

$scope.$watchGroup(['var1','var2'],function(newVals,oldVals){
   console.log("new value of var1 = " newVals[0]);
   console.log("new value of var2 = " newVals[1]);
   console.log("old value of var1 = " oldVals[0]);
   console.log("old value of var2 = " oldVals[1]);
});