AngularJS

Understanding $watch

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:

  1. type string, e.g., 'foo', it will be treated exactly like a $scope.foo case.
  2. type function, which gets passed into $parse(watchExp) without a second parameter, it’ll be returned as is with addInterceptor, and gets executed within each $digest, its returned value as the value to be compared against.
  3. other types, would get dealt with by addInterceptor(noop, interceptorFn) without a interceptorFn. And since noop 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).

Standard