2

I've written an AngularJS (1.x) directive that wraps a pure javascript library object, called browser.

In order to update Angular scope variables and view in response to events, occurring within browser, I have to manually call $apply, invoking the digest loop:

scope.browser.on({
    afterSetRange: function(){
        if (!scope.$$phase) scope.$apply();
    }
});

This works fine, when my directive is not creating its own scope - I say scope: false in Directive Definition Object (DDO). In that case scope refers to the page controller's $scope.

But when I'm using an isolated scope with scope: {myattr: '='}, this apparently doesn't save me from getting:

Error: [$rootScope:inprog] $digest is already in progress

I solved this problem by replacing if (!scope.$$phase) scope.$apply with $timout(angular.noop). (This means, that scope.$apply is poorly designed - it should just work, instead of forcing people to dig in its internals). What I really want is an asnwer to the following theoretical question, not practical help:


I don't understand the theory behind $digest loop. Documentation of rootScope.$new() implies that $digest event propagates from rootScope to its children. So, do we have a single digest loop for the whole angular app? Or a loop per each scope?

And how does Angular achieve synchronization between directive attributes and controller around it, when I'm using 2-way data binding in directive (e.g. scope: {myattr: '='})?

Boris Burkov
  • 13,420
  • 17
  • 74
  • 109
  • 1
    Firstly read this [Angular Antipatterns](https://github.com/angular/angular.js/wiki/Anti-Patterns) and don't use `if (!scope.$$phase) scope.$apply;`. Secondly, why you start using this construction ? – NechiK Mar 02 '17 at 12:45
  • 1
    Possible duplicate of [AngularJS : Prevent error $digest already in progress when calling $scope.$apply()](http://stackoverflow.com/questions/12729122/angularjs-prevent-error-digest-already-in-progress-when-calling-scope-apply) – lin Mar 02 '17 at 12:49
  • @NechiK I've read it and didn't understand. Can you elaborate on "Don't do if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack."? – Boris Burkov Mar 02 '17 at 12:53
  • 1
    Usually the one should know if the code is called during digest or not. If he/she doesn't, it probably indicates that he/she just don't know what's going on in his/her app. That's why it is an antipattern. It is not clear from your question if it really differs from the described situation. Btw, you don't call scope.$apply in the code above. Please, provide [MCVE](http://stackoverflow.com/help/mcve). – Estus Flask Mar 02 '17 at 13:10
  • Well, I don't know details of what's going on in a third-party 8000+ lines of bloody javascript - it's an asynchronous callback hell, written by 3-4 devs over 10 years. But that's unimportant - seems, I get 2 digests, called from different angular scopes. I won't dare re-creating a Complete, Minimal and Verifiable callback hell, but here's the code of my wrapper around it: https://github.com/RNAcentral/angularjs-genoverse/blob/master/src/genoverse.module.js#L170 – Boris Burkov Mar 02 '17 at 13:39
  • _"it should just work, instead of forcing people to dig in its internals"_ Um, you deliberately use `$$phase` - that's internal. If you want to start a digest cycle while one is in progress it's hardly Angular's fault. _"So, do we have a single digest loop for the whole angular app?"_ Yes. _"And how does Angular achieve synchronization between directive attributes and controller around it, when I'm using 2-way data binding in directive"_ It checks if a value has changed on one side and updates it on the other side. Not sure what you want to know. Also `scope.$apply;` does nothing. – a better oliver Mar 02 '17 at 13:58
  • 1
    How can you expect that someone will come up with solution to your problem if you can't explain or replicate it and refer it as 'hell'? It's not possible to say why there is this error in your case. But here's a couple of hints. `$scope.$evalAsync` should be used almost always instead of clumsy `$$phase` checks (I'm quite sure that dupe question covers this). And digest always propagates from root scope - **unless it was triggered specifically with `$scope.$digest()`** (can be used for good or for bad). – Estus Flask Mar 02 '17 at 13:59
  • @estus I don't expect anybody to solve the practical problem - I solved it myself already. But this example showed that I don't understand the theory. I'm asking about how digest loop handles nested scopes and how it handles isolated scope. – Boris Burkov Mar 02 '17 at 14:15
  • @zeroflagL I mean, `$apply()` should be idempotent - if one digest loop is already running, call to `$apply()` should've just `noop`ed. If there's only one $digest loop, why do I call `scope.$apply()` on a child scope, not `rootScope.$apply()`? And why when I was using controller's scope on a directive, checking `scope.$$phase` saved the day? How does $digest loop actually propagate through scope hierarchy? As for 2-way binding, if I say `scope.$watch('a', function(){ b=a;})`, `scope.$watch('b', function() {a=b;})`, isn't that an endless loop? – Boris Burkov Mar 02 '17 at 14:19
  • 1
    As I said, it propagates from root scope to nested scopes. A digest is just `for` loop that calls watch callbacks in all nested scopes. Isolated or not, scopes are children of their parents. $scope.$apply() essentially calls $rootScope.$apply() under the hood. It is semantically correct to use $scope. – Estus Flask Mar 02 '17 at 14:25
  • @estus Thank you. How do they implement two-way data binding on the border of directive with isolated scope? E.g. if you wanted to establish a 2-way dependency between controller and directive variables - for instance, you want to have `a == b+1` and you can change either `a` or`b`, so that other variable adapts. Can you just implement that as `scope.$watch('a', function(){ b=a;})`, `scope.$watch('b', function() {a=b;})`? Does the $digest loop stop as soon as both variables cease to change? – Boris Burkov Mar 02 '17 at 14:38
  • 1
    _"why do I call scope.$apply() on a child scope, not rootScope.$apply()"_ Because usually you don't have direct access to the root scope. _"if I say scope.$watch('a', function(){ b=a;}), scope.$watch('b', function() {a=b;}), isn't that an endless loop?"_ As soon as both have the same value no watcher function is called. – a better oliver Mar 02 '17 at 14:40
  • @BorisBurkov Yes, the digest will stop after the values are stablilized or will throw 'infinite digest' error after several iterations. Basic scope questions are possibly covered in the manual, https://docs.angularjs.org/guide/scope – Estus Flask Mar 02 '17 at 14:53
  • @estus Yeah, I've read it several times, thank you. – Boris Burkov Mar 02 '17 at 14:57

0 Answers0