mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-05-26 10:35:15 +08:00
docs(error/$rootScope/inprog): improve understanding and diagnosis of the error
See #5549
This commit is contained in:
@@ -3,72 +3,310 @@
|
||||
@fullName Action Already In Progress
|
||||
@description
|
||||
|
||||
At any point in time there can be only one `$digest` or $apply operation in progress.
|
||||
The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call.
|
||||
At any point in time there can be only one `$digest` or `$apply` operation in progress. This is to
|
||||
prevent very hard to detect bugs from entering your application. The stack trace of this error
|
||||
allows you to trace the origin of the currently executing `$apply` or `$digest` call, which caused
|
||||
the error.
|
||||
|
||||
`$digest` or `$apply` are processing operational states of the Scope - data-structure in Angular that provides context for models and enables model mutation observation.
|
||||
## Background
|
||||
|
||||
Trying to reenter a `$digest` or `$apply` while one of them is already in progress is typically a sign of programming error that needs to be fixed.
|
||||
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
|
||||
the processing of your application. The digest works by checking all the values that are being
|
||||
watched against their previous value and running any watch handlers that have been defined for those
|
||||
values that have changed.
|
||||
|
||||
This digest mechanism is triggered by calling `$digest` on a scope object. Normally you do not need
|
||||
to trigger a digest manually, because every external action that can trigger changes in your
|
||||
application, such as mouse events, timeouts or server responses, wrap the Angular application code
|
||||
in a block of code that will run `$digest` when the code completes.
|
||||
|
||||
You wrap Angular code in a block that will be followed by a `$digest` by calling `$apply` on a scope
|
||||
object. So, in pseudo-code, the process looks like this:
|
||||
|
||||
```
|
||||
element.on('mouseup', function() {
|
||||
scope.$apply(function() {
|
||||
$scope.doStuff();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
where `$apply()` looks something like:
|
||||
|
||||
```
|
||||
$apply = function(fn) {
|
||||
try {
|
||||
fn();
|
||||
} finally() {
|
||||
$digest();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Digest Phases
|
||||
|
||||
Angular keeps track of what phase of processing we are in, the relevant ones being `$apply` and
|
||||
`$digest`. Trying to reenter a `$digest` or `$apply` while one of them is already in progress is
|
||||
typically a sign of programming error that needs to be fixed. So Angular will throw this error when
|
||||
that occurs.
|
||||
|
||||
In most situations it should be well defined whether a piece of code will be run inside an `$apply`,
|
||||
in which case you should not be calling `$apply` or `$digest`, or it will be run outside, in which
|
||||
case you should wrap any code that will be interacting with Angular scope or services, in a call to
|
||||
`$apply`.
|
||||
|
||||
As an example, all Controller code should expect to be run within Angular, so it should have no need
|
||||
to call `$apply` or `$digest`. Conversely, code that is being trigger directly as a call back to
|
||||
some external event, from the DOM or 3rd party library, should expect that it is never called from
|
||||
within Angular, and so any Angular application code that it calls should first be wrapped in a call
|
||||
to $apply.
|
||||
|
||||
## Common Causes
|
||||
|
||||
Apart from simply incorrect calls to `$apply` or `$digest` there are some cases when you may get
|
||||
this error through no fault of your own.
|
||||
|
||||
### Inconsistent API (Sync/Async)
|
||||
|
||||
This error is often seen when interacting with an API that is sometimes sync and sometimes async.
|
||||
|
||||
For example:
|
||||
For example, imagine a 3rd party library that has a method which will retrieve data for us. Since it
|
||||
may be making an asynchronous call to a server, it accepts a callback function, which will be called
|
||||
when the data arrives.
|
||||
|
||||
```
|
||||
function MyController() {
|
||||
function MyController($scope, thirdPartyComponent) {
|
||||
thirdPartyComponent.getData(function(someData) {
|
||||
scope.$apply(function() {
|
||||
scope.someData = someData;
|
||||
$scope.$apply(function() {
|
||||
$scope.someData = someData;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The controller constructor is always instantiated from within an $apply cycle, so if the third-party component called our callback synchronously, we'd be trying to enter the $apply again.
|
||||
We expect that our callback will be called asynchronously, and so from outside Angular. Therefore, we
|
||||
correctly wrap our application code that interacts with Angular in a call to `$apply`.
|
||||
|
||||
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or wrap the call to the api with setTimeout call to make it always asynchronous.
|
||||
The problem comes if `getData()` decides to call the callback handler synchronously; perhaps it has
|
||||
the data already cached in memory and so it immediately calls the callback to return the data,
|
||||
synchronously.
|
||||
|
||||
Since, the `MyController` constructor is always instantiated from within an `$apply` call, our
|
||||
handler is trying to enter a new `$apply` block from within one.
|
||||
|
||||
Other situation that leads to this error is when you are trying to reuse a function to by using it as a callback for code that is called by various apis inside and outside of $apply.
|
||||
This is not an ideal design choice on the part of the 3rd party library.
|
||||
|
||||
For example:
|
||||
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or force
|
||||
your callback handler to always run asynchronously by using the `$timeout` service.
|
||||
|
||||
```
|
||||
myApp.directive('myDirective', function() {
|
||||
function MyController($scope, thirdPartyComponent) {
|
||||
thirdPartyComponent.getData(function(someData) {
|
||||
$timeout(function() {
|
||||
$scope.someData = someData;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Here we have used `$timeout` to schedule the changes to the scope in a future call stack.
|
||||
By providing a timeout period of 0ms, this will occur as soon as possible and `$timeout` will ensure
|
||||
that the code will be called in a single `$apply` block.
|
||||
|
||||
### Triggering Events Programmatically
|
||||
|
||||
The other situation that often leads to this error is when you trigger code (such as a DOM event)
|
||||
programmatically (from within Angular), which is normally called by an external trigger.
|
||||
|
||||
For example, consider a directive that will set focus on an input control when a value in the scope
|
||||
is true:
|
||||
|
||||
```
|
||||
myApp.directive('setFocusIf', function() {
|
||||
return {
|
||||
link: function($scope, $element) {
|
||||
function doSomeWork() {
|
||||
$scope.$apply(function() {
|
||||
// do work here, and update the model
|
||||
};
|
||||
}
|
||||
|
||||
$element.on('click', doSomeWork);
|
||||
doSomeWork(); // << this will throw an exception because templates are compiled within $apply
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
The fix for the example above looks like this:
|
||||
```
|
||||
myApp.directive('myDirective', function() {
|
||||
return {
|
||||
link: function($scope, $element) {
|
||||
function doSomeWork() {
|
||||
// do work here, and update the model
|
||||
}
|
||||
|
||||
$element.on('click', function() {
|
||||
$scope.$apply(doSomeWork); // <<< the $apply call was moved to the callsite that doesn't execute in $apply call already
|
||||
link: function($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) { $element[0].focus(); }
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
doSomeWork();
|
||||
If we applied this directive to an input which also used the `ngFocus` directive to trigger some
|
||||
work when the element receives focus we will have a problem:
|
||||
|
||||
```
|
||||
<input set-focus-if="hasFocus" ng-focus="msg='has focus'">
|
||||
<button ng-click="hasFocus = true">Focus</button>
|
||||
```
|
||||
|
||||
In this setup, there are two ways to trigger ngFocus. First from a user interaction:
|
||||
|
||||
* Click on the input control
|
||||
* The input control gets focus
|
||||
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
|
||||
`$apply()`
|
||||
|
||||
Second programmatically:
|
||||
|
||||
* Click the button
|
||||
* The `ngClick` directive sets the value of `$scope.hasFocus` to true inside a call to `$apply`
|
||||
* The `$digest` runs, which triggers the watch inside the `setFocusIf` directive
|
||||
* The watch's handle runs, which gives the focus to the input
|
||||
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
|
||||
`$apply()`
|
||||
|
||||
In this second scenario, we are already inside a `$digest` when the ngFocus directive makes another
|
||||
call to `$apply()`, causing this error to be thrown.
|
||||
|
||||
It is possible to workaround this problem by moving the call to set the focus outside of the digest,
|
||||
by using `$timeOut(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in a
|
||||
`$apply` block:
|
||||
|
||||
```
|
||||
myApp.directive('setFocusIf', function($timeout) {
|
||||
return {
|
||||
link: function($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) {
|
||||
$timeout(function() {
|
||||
// We must reevaluate the value in case it was changed by a subsequent
|
||||
// watch handler in the digest.
|
||||
if ( $scope.$eval($attr.setFocusIf) ) {
|
||||
$element[0].focus();
|
||||
}
|
||||
}, 0, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
To learn more about Angular processing model please check out the {@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
|
||||
## Diagnosing This Error
|
||||
|
||||
When you get this error it can be rather daunting to diagnose the cause of the issue. The best
|
||||
course of action is to investigate the stack trace from the error. You need to look for places
|
||||
where `$apply` or `$digest` have been called and find the context in which this occurred.
|
||||
|
||||
There should be two calls:
|
||||
|
||||
* The first call is the good `$apply`/`$digest` and would normally be triggered by some event near
|
||||
the top of the call stack.
|
||||
|
||||
* The second call is the bad `$apply`/`$digest` and this is the one to investigate.
|
||||
|
||||
Once you have identified this call you work your way up the stack to see what the problem is.
|
||||
|
||||
* If the second call was made in your application code then you should look at why this code has been
|
||||
called from within a `$apply`/`$digest`. It may be a simple oversight or maybe it fits with the
|
||||
sync/async scenario described earlier.
|
||||
|
||||
* If the second call was made inside an Angular directive then it is likely that it matches the second
|
||||
programmatic event trigger scenario described earlier. In this case you may need to look further up
|
||||
the tree to what triggered the event in the first place.
|
||||
|
||||
### Example Problem
|
||||
|
||||
Let's look at how to investigate this error using the `setFocusIf` example from above. This example
|
||||
defines a new `setFocusIf` directive that sets the focus on the element where it is defined when the
|
||||
value of its attribute becomes true.
|
||||
|
||||
<example name="error-$rootScope-inprog" module="app">
|
||||
<file name="index.html">
|
||||
<button ng-click="focusInput = true">Focus</button>
|
||||
<input ng-focus="count = count + 1" set-focus-if="focusInput" />
|
||||
</file>
|
||||
<file name="app.js">
|
||||
angular.module('app', []).directive('setFocusIf', function() {
|
||||
return function link($scope, $element, $attr) {
|
||||
$scope.$watch($attr.setFocusIf, function(value) {
|
||||
if ( value ) { $element[0].focus(); }
|
||||
});
|
||||
};
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
When you click on the button to cause the focus to occur we get our `$rootScope:inprog` error. The
|
||||
stacktrace looks like this:
|
||||
|
||||
```
|
||||
Error: [$rootScope:inprog]
|
||||
at Error (native)
|
||||
at angular.min.js:6:467
|
||||
at n (angular.min.js:105:60)
|
||||
at g.$get.g.$apply (angular.min.js:113:195)
|
||||
at HTMLInputElement.<anonymous> (angular.min.js:198:401)
|
||||
at angular.min.js:32:32
|
||||
at Array.forEach (native)
|
||||
at q (angular.min.js:7:295)
|
||||
at HTMLInputElement.c (angular.min.js:32:14)
|
||||
at Object.fn (app.js:12:38) angular.js:10111
|
||||
(anonymous function) angular.js:10111
|
||||
$get angular.js:7412
|
||||
$get.g.$apply angular.js:12738 <--- $apply
|
||||
(anonymous function) angular.js:19833 <--- called here
|
||||
(anonymous function) angular.js:2890
|
||||
q angular.js:320
|
||||
c angular.js:2889
|
||||
(anonymous function) app.js:12
|
||||
$get.g.$digest angular.js:12469
|
||||
$get.g.$apply angular.js:12742 <--- $apply
|
||||
(anonymous function) angular.js:19833 <--- called here
|
||||
(anonymous function) angular.js:2890
|
||||
q angular.js:320
|
||||
```
|
||||
|
||||
We can see (even though the Angular code is minified) that there were two calls to `$apply`, first
|
||||
on line `19833`, then on line `12738` of `angular.js`.
|
||||
|
||||
It is this second call that caused the error. If we look at the angular.js code, we can see that
|
||||
this call is made by an Angular directive.
|
||||
|
||||
```
|
||||
var ngEventDirectives = {};
|
||||
forEach(
|
||||
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
|
||||
function(name) {
|
||||
var directiveName = directiveNormalize('ng-' + name);
|
||||
ngEventDirectives[directiveName] = ['$parse', function($parse) {
|
||||
return {
|
||||
compile: function($element, attr) {
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function(scope, element, attr) {
|
||||
element.on(lowercase(name), function(event) {
|
||||
scope.$apply(function() {
|
||||
fn(scope, {$event:event});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
It is not possible to tell which from the stack trace, but we happen to know in this case that it is
|
||||
the `ngFocus` directive.
|
||||
|
||||
Now look up the stack to see that our application code is only entered once in `app.js` at line `12`.
|
||||
This is where our problem is:
|
||||
|
||||
```
|
||||
10: link: function($scope, $element, $attr) {
|
||||
11: $scope.$watch($attr.setFocusIf, function(value) {
|
||||
12: if ( value ) { $element[0].focus(); } <---- This is the source of the problem
|
||||
13: });
|
||||
14: }
|
||||
```
|
||||
|
||||
We can now see that the second `$apply` was caused by us programmatically triggering a DOM event
|
||||
(i.e. focus) to occur. We must fix this by moving the code outside of the $apply block using
|
||||
`$timeout` as described above.
|
||||
|
||||
## Further Reading
|
||||
To learn more about Angular processing model please check out the
|
||||
{@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
|
||||
|
||||
Reference in New Issue
Block a user