In what order do angular watchers and event listeners execute?
Asked Answered
M

1

7

If one changes a scope property first, and then broadcasts an event second, will the corresponding watcher callback and event listeners callback always be executed in that same order? For example:

$scope.foo = 3;
$scope.$broadcast('bar');

and elsewhere:

$scope.$watch('foo', function fn1(){...});
$scope.$on('bar', function fn2(){...});

Will fn1 always be executed prior to fn2, or visa-versa, or can the order not be relied upon? Please cite sources, preferably to official angular docs.

In case it matters: lets assume the $scope.foo= and the $broadcast occur in a function invoked by an ng-click (i.e. user interaction)

[aside] Sorry question title is sloppy - please rename if you have something better.

Musky answered 29/12, 2015 at 21:2 Comment(5)
Nothing official but I'm pretty sure that's all handled asynchronously so you cannot be guaranteed of any order. Race condition.Graeae
@Graeae thanks, that's my suspicion too. If the broadcast is called in a $timeout(fn, 0), can we be assured that the callback for the $on executes after then $watch callback?Musky
I'm not sure there. It's possible that just lessens the effects of the race condition. Better to just chain promises. Or other callbacks.Graeae
right, a lessened race condition just makes it harder to find! Maybe I'll ask a separate question about that.Musky
See my updated answer below. I am fairly certain the event handler gets called before the watch function.Phenolphthalein
P
6

To understand what is happening, you need to understand Angular's $digest cycle and event $emit and $broadcast functions.

Based on some research, I've also learned that Angular does not use any kind of polling mechanism to periodically check for model changes. This is not explained in the Angular docs, but can be tested (see this answer to a similar question).

Putting all of that together, I wrote a simple experiment and concluded that you can rely on your event handlers running first, then your watch functions. Which makes sense, because the watch functions can be called several times in succession during the digest loop.

The following code...

template.html

<div ng-app="myApp">
  <div watch-foo ng-controller="FooController">
    <button ng-click="changeFoo()">
      Change
    </button>
  </div>
</div>

script.js

angular.module('myApp', [])
  .directive('watchFoo', watchFooDirective)
  .controller('FooController', FooController);

function watchFooDirective($rootScope) {
  return function postLink(scope) {
    scope.$watch(function () {
        return scope.foo;
    }, function (value) {
        console.log('scope.$watch A');
    });
    scope.$on('foo', function (value) {
        console.log('scope.$on A');
    });
    $rootScope.$on('foo', function (value) {
        console.log('$rootScope.$on A');
    });
    $rootScope.$on('foo', function (value) {
        console.log('$rootScope.$on B');
    });
    scope.$on('foo', function (value) {
        console.log('scope.$on B');
    });
    scope.$watch(function () {
        return scope.foo;
    }, function (value) {
        console.log('scope.$watch B');
    });
  };
}

function FooController($scope) {
  $scope.foo = 'foo';
  $scope.changeFoo = function() {
    $scope.foo = 'bar';
    $scope.$emit('foo');
  };
}

...yields the following results in the console when the 'Change' button is clicked:

scope.$on A
scope.$on B
$rootScope.$on A
$rootScope.$on B
scope.$watch A
scope.$watch B

UPDATE

Here is another test that illustrates the watch callback being called twice in the digest loop, but the event handlers not being called a second time: https://jsfiddle.net/sscovil/ucb17tLa/

And a third test that emits an event inside the watch function, then updates the value being watched: https://jsfiddle.net/sscovil/sx01zv3v/

In all cases, you can rely on the event listeners being called before the watch functions.

Phenolphthalein answered 29/12, 2015 at 21:33 Comment(5)
Thanks. I have read the docs on $digest, and I don't actually see anything to support your statement, "when you update the value of foo the digest loop runs". Are you suggesting that the digest loop runs immediately after foo is set, and before the currently executing function returns? I wasn't aware of that, and it doesn't match the tests I've done. Please could you quote the specific section of the docs that supports that? My impression is that the digest cycle runs some time after the function returns, I'm just not sure how the $broadcast fits into that timing.Musky
Actually, you are right. The digest doesn't run immediately after the value of foo is updated, as is illustrated in the console of this test: jsfiddle.net/sscovil/jj753wqf -- I will look into it further and update my answer.Phenolphthalein
thanks shaun. this is certainly a valid test, though i'm not convinced that it proves that the order will always be so. it's a good start, but i wouldn't rely on this in production w/o either gathering a lot more data points, or (even better) examining the angular code (or docs) to prove that it will always be the case. but that's just my style :) thanks again for making the test - at very least they are a starting point for examining the code.Musky
Np. You might also want to read up on $evalAsync, which is used to run watch functions. Looks like they do run asynchronously after all, if I'm reading it correctly. docs.angularjs.org/api/ng/type/$rootScope.Scope#$evalAsyncPhenolphthalein
After further testing, I've concluded that the watch functions run one at a time, in the order they are registered. Same with event handlers. Nothing is being run asynchronously. Here is a test that I believe proves it: jsfiddle.net/sscovil/sggrdhbn -- notice that function 1 finishes before function 2 starts, first for the scope event handler, then the rootScope, then the watch function.Phenolphthalein

© 2022 - 2024 — McMap. All rights reserved.