Angularjs:Difference Between the $Observe and $Watch Methods

AngularJS : Difference between the $observe and $watch methods

$observe() is a method on the Attributes object, and as such, it can only be used to observe/watch the value change of a DOM attribute. It is only used/called inside directives. Use $observe when you need to observe/watch a DOM attribute that contains interpolation (i.e., {{}}'s).

E.g., attr1="Name: {{name}}", then in a directive: attrs.$observe('attr1', ...).

(If you try scope.$watch(attrs.attr1, ...) it won't work because of the {{}}s -- you'll get undefined.) Use $watch for everything else.

$watch() is more complicated. It can observe/watch an "expression", where the expression can be either a function or a string. If the expression is a string, it is $parse'd (i.e., evaluated as an Angular expression) into a function. (It is this function that is called every digest cycle.) The string expression can not contain {{}}'s. $watch is a method on the Scope object, so it can be used/called wherever you have access to a scope object, hence in

  • a controller -- any controller -- one created via ng-view, ng-controller, or a directive controller
  • a linking function in a directive, since this has access to a scope as well

Because strings are evaluated as Angular expressions, $watch is often used when you want to observe/watch a model/scope property. E.g., attr1="myModel.some_prop", then in a controller or link function: scope.$watch('myModel.some_prop', ...) or scope.$watch(attrs.attr1, ...) (or scope.$watch(attrs['attr1'], ...)).

(If you try attrs.$observe('attr1') you'll get the string myModel.some_prop, which is probably not what you want.)

As discussed in comments on @PrimosK's answer, all $observes and $watches are checked every digest cycle.

Directives with isolate scopes are more complicated. If the '@' syntax is used, you can $observe or $watch a DOM attribute that contains interpolation (i.e., {{}}'s). (The reason it works with $watch is because the '@' syntax does the interpolation for us, hence $watch sees a string without {{}}'s.) To make it easier to remember which to use when, I suggest using $observe for this case also.

To help test all of this, I wrote a Plunker that defines two directives. One (d1) does not create a new scope, the other (d2) creates an isolate scope. Each directive has the same six attributes. Each attribute is both $observe'd and $watch'ed.

<div d1 attr1="{{prop1}}-test" attr2="prop2" attr3="33" attr4="'a_string'"
attr5="a_string" attr6="{{1+aNumber}}"></div>

Look at the console log to see the differences between $observe and $watch in the linking function. Then click the link and see which $observes and $watches are triggered by the property changes made by the click handler.

Notice that when the link function runs, any attributes that contain {{}}'s are not evaluated yet (so if you try to examine the attributes, you'll get undefined). The only way to see the interpolated values is to use $observe (or $watch if using an isolate scope with '@'). Therefore, getting the values of these attributes is an asynchronous operation. (And this is why we need the $observe and $watch functions.)

Sometimes you don't need $observe or $watch. E.g., if your attribute contains a number or a boolean (not a string), just evaluate it once: attr1="22", then in, say, your linking function: var count = scope.$eval(attrs.attr1). If it is just a constant string – attr1="my string" – then just use attrs.attr1 in your directive (no need for $eval()).

See also Vojta's google group post about $watch expressions.

Difference between these watch methods in angularJS?

See "Scope $watch Depths" at https://docs.angularjs.org/guide/scope. Within is a particularly helpful graphic:

Sample Image

Targeting a $scope property that is an 'object with Array properties' (if I understand the question correctly), I've demonstrated a few of the different $watcher configurations in the snippet below so that you can see the various behaviours in action.

(function() {  "use strict";
angular.module('myApp', []) .controller("Controller1", ['$scope', Controller1]);
function Controller1($scope) { var _objWithArrayProps = { 'prop1': [], 'prop2': [], 'prop3': [] }, _counts = { 'watchNoObjEquality': -1, //watchers to -1 since they fire once upon initialisation 'watchPropNoObjEquality': -1, 'watchObjEquality': -1, 'watchFunctionNoObjEquality': -1, 'watchFunctionObjEquality': -1, 'watchCollection': -1, 'watchCollectionFunction': -1, 'clicks': 0 };
$scope.data = { 'objWithArrayProps': _objWithArrayProps, 'counts': _counts };
$scope.$watch('data.objWithArrayProps', watchNoObjEquality); $scope.$watch('data.objWithArrayProps.prop1', watchPropNoObjEquality); $scope.$watch('data.objWithArrayProps', watchObjEquality, true); $scope.$watch(watchFunction, watchFunctionNoObjEquality); $scope.$watch(watchFunction, watchFunctionObjEquality, true); $scope.$watchCollection('data.objWithArrayProps', watchCollection); $scope.$watchCollection(watchFunction, watchCollectionFunction);
$scope.addProperty = addProperty; $scope.addArrayElements = addArrayElements; $scope.modifyProperties = modifyProperties; $scope.modifyArrayElements = modifyArrayElements;
function watchNoObjEquality(newVal, oldVal) { _counts.watchNoObjEquality++; }
function watchPropNoObjEquality(newVal, oldVal) { _counts.watchPropNoObjEquality++; }
function watchObjEquality(newVal, oldVal) { _counts.watchObjEquality++; }
function watchFunction() { return _objWithArrayProps; // same as: return $scope.data.objWithArrayProps; }
function watchFunctionNoObjEquality(newVal, oldVal) { _counts.watchFunctionNoObjEquality++; }
function watchFunctionObjEquality(newVal, oldVal) { _counts.watchFunctionObjEquality++; }
function watchCollection(newVal, oldVal) { _counts.watchCollection++; }
function watchCollectionFunction(newVal, oldVal) { _counts.watchCollectionFunction++; }
var _propNameNumberSuffix = 4; //for naming properties incrementally function addProperty() { _counts.clicks++; _objWithArrayProps['prop' + _propNameNumberSuffix] = []; _propNameNumberSuffix++; }
function addArrayElements() { _counts.clicks++; for (var prop in _objWithArrayProps) { if (_objWithArrayProps.hasOwnProperty(prop)) { _objWithArrayProps[prop].push('x'); } } }
function modifyProperties() { _counts.clicks++; for (var prop in _objWithArrayProps) { if (_objWithArrayProps.hasOwnProperty(prop)) { _objWithArrayProps[prop] = [Math.random()]; //set to a fresh array with a random number in it } } }
function modifyArrayElements() { _counts.clicks++; for (var prop in _objWithArrayProps) { if (_objWithArrayProps.hasOwnProperty(prop)) { //ensure we have at least one item in array(s) if (!_objWithArrayProps[prop].length) { _objWithArrayProps[prop].push('x'); }
for (var i = 0, iLen = _objWithArrayProps[prop].length; i < iLen; i++) { _objWithArrayProps[prop][i] = Math.random().toFixed(4); } } } }
}
})();
body {  font-family: "Arial", "sans-serif";  font-size: 0.8em;}h1 {  font-size: 1.4em;}pre {  margin: 0;}pre code {  display: block;  background-color: #eee;  padding: 5px;  max-height: 200px;  overflow: auto;}ul.tab li span {  display: inline-block;  margin: 5px 0;}ul.tab li {  border-bottom: 1px dashed #ddd;  display: block;  padding: 0;}ul.tab {  padding: 0;}#data,#counts {  width: 45%;  display: inline-block;  vertical-align: top;  margin: 2%;}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script><div ng-app="myApp" ng-controller="Controller1">  <h1>Object with Array properties</h1>  <div id='counts'>    <strong>Watcher fired count:</strong>    <ul class="tab">      <li><span><strong>$watch</strong>('objWithArrayProps', ...) :</span> {{data.counts.watchNoObjEquality}}</li>      <li><span><strong>$watch</strong>('objWithArrayProps.prop1', ...) :</span> {{data.counts.watchPropNoObjEquality}}</li>      <li><span><strong>$watch</strong>('objWithArrayProps', ..., true) :</span> {{data.counts.watchObjEquality}}</li>      <li><span><strong>$watch</strong>(function() {...}, ...) :</span> {{data.counts.watchFunctionNoObjEquality}}</li>      <li><span><strong>$watch</strong>(function() {...}, ..., true) :</span> {{data.counts.watchFunctionObjEquality}}</li>      <li><span><strong>$watchCollection</strong>('objWithArrayProps', ...) :</span> {{data.counts.watchCollection}}</li>      <li><span><strong>$watchCollection</strong>(function() {...}, ...) :</span> {{data.counts.watchCollectionFunction}}</li>    </ul>    <strong>Button clicks:</strong> {{data.counts.clicks}}  </div>  <div id='data'>    <pre><code>{{data.objWithArrayProps | json}}</code></pre>  </div>  <button ng-click="addProperty()">Add a property</button>  <button ng-click="addArrayElements()">Add array elements</button>  <button ng-click="modifyProperties()">Modify properties</button>  <button ng-click="modifyArrayElements()">Modify array elements</button></div>

What's the difference between these watch expressions?


$scope.$watch('foo', fn)

This will use the $parse service to watch the value of $scope.foo, and will compare old values against new.


$scope.$watch(function() {return $scope.foo}, fn)

This is the same as the first, but uses a lambda. The function() {return $scope.foo} will be executed on each $digest, the old return values will be compared with return new.


$scope.$watch(obj.prop, fn)

This one is weird and not-recommended, because its behavior depends entirely on the type of obj.prop. For example if obj.prop === "foo", then it will be the same as $scope.$watch('foo', fn). If obj.prop === function(){ return Math.random(); } then you got a weird thing. If you are expecting angular to $watch the value of obj.prop for changes to obj.prop, it wont work this way.


$scope.$watch(function() {return obj.prop}, fn)

This one is the same as $scope.$watch(function() {return $scope.foo}, fn) in that angular will run the lambda function() {return obj.prop} each $digest. Old return values will be compared to the new. What you have here is ultimately to proper way to watch something that is not on $scope. It will monitor obj.prop for changes (because its the return value).

Using scope.$watch to observe when a function executes

Here are two ways to solve your problem that I could think of.

1. Use a counter

You can always increment a counter when invoking my_function:

// This will reside in your controller
$scope.counter = 0;
$scope.my_function = function () {
// do your logic
$scope.counter++;
};

// This will reside in your directive
$scope.$watch( "counter", function () {
// do something
});

2. Simply watch my_data

Assuming my_function will always override the my_data variable, you can use the default Angular behavior for $watch. You don't have to care if your backend has news for you or not, the reference will never be the same.

// in your controller
$scope.my_function = function () {
$http.get(...).then(function ( response ) {
$scope.my_data = response.data;
});
};

// in your directive
$scope.$watch( "my_data", function () {
// do something
});

Obviously, this is the cleaner way to do this, but you may get in trouble if you don't unit test because of one of the following:

  • You add any sort of logic around my_data that may conflict with your watcher;
  • You want to cache your HTTP calls.

I hope I was able to help you! Good luck.

Execute $watch or $observe once

Thanks to both answers!

I mixed both and reached a solution.

var myFunctionWait = function(){
// wait a bit, to give $observe a chance to fire.
setTimeout(function(){ myFunction(); }, 100);
}

var myAttrFunction = function(){
unbindWatch();
myFunction();
}

var unbindWatch = scope.$parent.$watch('Parent.Var', myFunctionWait );
attrs.$observe('myAttr', myAttrFunction);

Unfortunately i do not have reputation yet to upvote :(

Angular Directive attrs.$observe

In short:

Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.

From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)

$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.

From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)

$compile.directive.Attributes#$observe(key, fn);

Observes an interpolated attribute.

The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.



Related Topics



Leave a reply



Submit