Could you tell the difference between these four?
$scope.$watch('foo', fn)
$scope.$watch(function() {return $scope.foo}, fn)
$scope.$watch(obj.prop, fn)
$scope.$watch(function() {return obj.prop}, fn)
To better understand how $watch works, I had a look at the code, and this is how it goes down:
$watch
$watch: function(watchExp, listener, objectEquality) {
var get = $parse(watchExp);
if (get.$$watchDelegate) {
return get.$$watchDelegate(this, listener, objectEquality, get);
}
var scope = this,
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: watchExp,
eq: !!objectEquality
};
lastDirtyWatch = null;
if (!isFunction(listener)) {
watcher.fn = noop;
}
if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
},
So when the comparison executes, to see if (value = watch.get(current)) !== (last = watch.last)
, which is basically calling the get
in the second line of $watch
and compare its new value with the stored one. To know what get
is, I had a look at $parse
:
$parse
return function $parse(exp, interceptorFn) {
var parsedExpression, oneTime, cacheKey;
switch (typeof exp) {
case 'string':
cacheKey = exp = exp.trim();
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
oneTime = true;
exp = exp.substring(2);
}
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
} else if (oneTime) {
//oneTime is not part of the exp passed to the Parser so we may have to
//wrap the parsedExpression before adding a $$watchDelegate
parsedExpression = wrapSharedExpression(parsedExpression);
parsedExpression.$$watchDelegate = parsedExpression.literal ?
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
case 'function':
return addInterceptor(exp, interceptorFn);
default:
return addInterceptor(noop, interceptorFn);
}
};
Basically, it has different cases for what the watched expression’s type is. It has string
, function
, anddefault
.
What it means is that if my watched expression obj.prop
evaluated to:
- type
string
, e.g.,'foo'
, it will be treated exactly like a$scope.foo
case. - type
function
, which gets passed into$parse(watchExp)
without a second parameter, it’ll be returned as is withaddInterceptor
, and gets executed within each$digest
, its returned value as the value to be compared against. - other types, would get dealt with by
addInterceptor(noop, interceptorFn)
without ainterceptorFn
. And sincenoop
is just an empty angular function, this one wouldn’t do anything.
Conclusion
So if I’d wanted to check a obj.prop
that’s not part of the $scope, I should go with the function returning value route, the $scope.$watch(function() {return obj.prop}, fn)
.