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:
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
How to Change the Background Image of Div Using JavaScript
JavaScript Detect Click Event Outside of Div
JavaScript Error Null Is Not an Object
Mui Createtheme Is Not Properly Passing Theme to Mui Components
How to Optimize Website for Touch Devices
How to Do Page Numbering in Header/Footer HTMLs with Wkhtmltopdf
Jquery Parallax Scrolling Effect - Multi Directional
Ios8 Safari After a Pushstate the :Nth-Child() Selectors Not Works
How to Access Styles from React
How to Zoom a Background Image on a Div with Background-Size
Calculate Color Values from Green to Red
How to Inject a Style with an "!Important" Rule
How to Make My Navbar Change CSS Class Upon Scrolling Past an Anchor Point
Using Masonry with Imagesloaded
Delay Removing a Class in Jquery
Find the "Potential" Width of a Hidden Element