Quick question about the $interval service in angular. Looking at the docs ($interval) they warn you to manually cancel intervals, but you have the option of providing a count parameter upon initialization. Once the timer has "ticked" past the allotted count does it cancel itself or simply stop calling the function and live on in the background?
TL;DR; After the count, the interval is cleared.
As the same documentation says, it is recommended that you cancel the $interval when the scope of your controller is detroyed. Something like:
var t = $interval(function(){
...
}, 1000);
$scope.$on('$destroy', function(){
$interval.cancel(t);
});
The delay parameter is time interval which the function is called. In the example above, the function is called every 1000 milliseconds. If you don't cancel the $interval, Angular will hold a reference to it, and may continue to execute your function, causing strange behaviors in your app.
Considering that the $interval provider is just a wrapper of the native setInterval(), with the adition of the $apply, looking at the Angular implementation (https://github.com/angular/angular.js/blob/master/src/ng/interval.js), we can find this snippet of code:
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
}
So, the promise created by the provider is resolved, and the interval is cleared. The cancel method does this:
intervals[promise.$$intervalId].reject('canceled');
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
So, I think your assumption is right. After the count, the interval is already cleared.
Related
I am trying to implement a function to check for user inactivity. My online search has suggested that I can use the $rootScope.$watch to monitor activity. However, I am a bit concerned about the $digest trigger... as I can't seem to be able to find exactly when or which events trigger this. Any pointers will be appreciated.
Angular's $digest alone isn't going to solve the requirement, in my opinion. What if the user moves their mouse, shouldn't that restart the idle timer?
What I recommend is making a directive that creates an event listener on the $window object for both keydown and mousemove. Then create a service that will allow other components to receive notification when the user's idle state changes.
Something like this:
angular
.module('myApp', [])
.service('Idle', function(){
var self = {
callbacks: {
onidle: []
},
on: function(event, callback){
event = 'on' + event.toLowerCase();
if(self.callbacks[event] && typeof callback === 'function'){
self.callbacks[event].push(callback);
}
return this;
},
trigger: function(event){
event = 'on' + event.toLowerCase();
if(self.callbacks[event])
{
lodash.each(self.callbacks[event], function(callback){
callback.call();
});
}
return this;
}
}
return {
on: self.on,
trigger: self.trigger
}
})
.directive('idleNotification',
function(
$window,
Idle
){
return {
link: function($scope){
var timer = setTimer();
angular
.element($window)
.on('keydown mousemove', function(){
clearTimeout(timer);
timer = setTimer();
});
function setTimer(){
return setTimeout(function(){
Idle.trigger('idle');
}, 30);
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Then use the Idle service like so:
Idle.on('idle', function(){
// do something here when the user becomes idle
});
Angular triggers $digest (generally) all by itself. Your $watch should "just work".
Oh - if you really need to initiate the $digest loop yourself, you should use $scope.apply() instead of calling digest directly.
What really happens behind the scenes though is all kinds of things actually trigger a $digest, including ng-click events, timeouts, watchers, long-polling, websockets, http calls, promises - anything that changes the internal application state that angular manages (via "dirty checking"). For more info, see here: https://www.sitepoint.com/understanding-angulars-apply-digest/ and https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest
To understand the Angular context, we need to look at exactly what
goes on inside of it. There are two major components of the $digest
loop:
When Angular thinks that the value could change, it will call $digest() on the value to check whether the value is “dirty.” Hence, when the Angular runtime is running, it will look for potential changes on the value.
$watch lists are resolved in the $digest loop through a process called dirty checking, so after the scope expression is evaluated and the $digest loop runs, the $scope’s watch expressions will run dirty checking.
During the $digest cycle, which executes on the $rootScope, all of the children scopes will perform dirty digest checking. All of the watching expressions are checked for any changes, and the scope calls the listener callback when they are changed.
For example
When we call the $http method, it won’t actually execute until the next $digest loop runs. Although most of the time we’ll be calling $http inside of an $apply block, we can also execute the method outside of the Angular digest loop.
To execute an $http function outside of the $digest loop, we need to wrap it in an $apply block.
That will force the digest loop to run, and our promises will resolve as we expect them to.
$scope.$apply(function() {
$http({
method: 'GET',
url: '/api/dataFile.json'
});
});
Hope this much understanding help!
What is the correct way to do have elements update regularly, without explicitly doing that for each variable in an $interval callback?
This would be useful in the following situation: an element's content is bound to a function, like this:
<p ng-bind="timeLeft()"></p>
The timeleft() function returns a value depending on the current time. It needs to be refreshed every 20 ms.
Edit: usually ngBind updates just fine, but I guess because the outcome of timeLeft() only depends on time passing, in this case it doesn't. Without the code below, the view is not updated after loading the DOM, at least not without updating other values.
I've tried the following, which works:
$scope.updateView = function(){
if(!$scope.$$phase) { // To prevent this: "Error: [$rootScope:inprog] $digest already in progress"
$scope.apply();
}
}
$interval( function(){
$scope.updateView();
}, 50);
I noticed just using $interval may already suffice, in my setup this works too:
$scope.updateView = function(){
}
$interval( function(){
$scope.updateView();
}, 20);
Would it be reliable to use that last solution?
Yes, you should be reliable with second solution.
Angular does not run the digest cycle when event occurred from outside angular context, at that you need to tell AngularJs to run apply cycle to update all bindings. When you are using $interval angular will run digest cycle after an execution of callback function, there is no need of running digest cycle which you have done in your 1st solution.
You need to look at $interval service
$interval($scope.updateView,
20, //delay
10, //this option will run it 10 times, if not specified then infinite times
true, //do you want to run digest cycle after callback execution, defaulted to true
{} //any extra params
);
There is this article I saw long time ago:
https://coderwall.com/p/ngisma
It describes a method that triggers $apply if we are not in an apply or digest phase.
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
Angular has the $scope.$evalAsync method (taken from 1.2.14):
$evalAsync: function(expr) {
// if we are outside of an $digest loop and this is the first time we are scheduling async
// task also schedule async auto-flush
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
Which calls digest if we are not in a phase and adds the current invocation to the asyncQueue.
There is also the $apply, $digest and $timeout methods.
It is confusing.
What is the difference between all ways mentioned to trigger a digest cycle (a dirty check and data binding)?
What is the use case for each method?
Is safeApply() still safe? :)
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
As a high level introduction I would say that it is rarely required to actually initiate your own digest cycle, since angular handles most cases.
That being said let's dive into the question.
As a very high level, the $digest loop looks like this:
Do:
- - - If asyncQueue.length, flush asyncQueue.
- - - Trigger all $watch handlers.
- - - Check for "too many" $digest iterations.
While: ( Dirty data || asyncQueue.length )
So basically $evalAsync is adding the function to the asyncQueue and defering a digest if it needs to. However if it's already in a digest cycle it will flush the asyncQueue and it will just call the function.
You may notice that this is very similar to the safeApply. One difference is that instead of adding the function to the asyncQueue it just calls it, which can happen in the middle of a cycle for instance. The other difference is that it exposes and relies on a $$ variable, which are intended to be internal.
The most important difference however between $evalAsync and $apply from $digest (I will get to $timeout below) is that $evalAsync and $apply starts the digest at the $rootScope but you call the $digest on any scope. You will need to evaluate your individual case if you think you need this flexibility.
Using $timeout is very similar to $evalAsync again except that it will always defer it out of the current digest cycle, if there is one.
$timeout(function() {
console.log( "$timeout 1" );
});
$scope.$evalAsync(function( $scope ) {
console.log( "$evalAsync" );
});
If you already in a digest cycle will give you
$evalAsync
$timeout 1
Even though they are called in the opposite order, since the timeout one is delayed to the next digest cycle, which it instantiates.
EDIT For the questions in the comment.
The biggest difference between $apply and $evalAsync as far as I can tell is that $apply is actually triggering the $digest cycle. To you all this means is that you need to be sure you call $apply when you aren't in a digest cycle. Just for transparency, the following code is also valid:
$scope.$apply(function() {
$scope.$evalAsync(function() {
});
});
Which will call the apply function, add the no-op function to the $asyncQueue and begin the digest cycle. The docs say it is recommended to use $apply whenever the model changes, which could happen in your $evalAsync
The difference between fn(); $scope.apply() and $scope.apply(fn) is just that the $scope.apply(fn) does a try catch for the function and explicitly uses $exceptionHandler. Additionally, you could actually attempt to cause digest cycles in fn, which will change how the apply is handled.
I'd also like to point out at this time that it is even more complicated in 1.3 assuming it stays this way. There will be a function called $applyAsync used to defer apply calls (reference).
This post compiled some information from This blog and this so post plus some of my experience.
Hope this helped!
There's only one way to start a digest cycle: Calling $digest. It's rarely called directly.
$apply is essentially a safe wrapper for $digest. Safe means that it handles errors. So most of the time this is the method you want to call to propagate changes. Another important difference is that it always starts a digest cycle at the root scope, whereas $digest can be called on any scope and only affects that scope and its descendants.
Many services and directives trigger a digest cycle, usually by calling $apply. The only important thing to know is wether you have to call $apply yourself or it has already been called by the service / directive.
Is safeApply still safe?
Yes, because there's still only one way to start a digest cycle.
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?
Not calling $apply in the middle of a digest cycle. Seriously. Being in that situation is almost always a design issue in the first place. It's better to resolve that issue instead of covering it.
Anyway the method used by the solution, checking $$phase, is the only way of knowing if a digest cycle is running. As shown by you it's even used by Angular internally. It's enough to do if (!$rootScope.$$phase). But keep in mind that it's a hack that might break with a new version of Angular. BTW: Monkey patching the topmost scope, as proposed by the author, would make the solution useless for isolated scopes.
Angular's way of data binding might indeed be difficult to understand for someone new to Angular at first. But basically it all boils down to this: For every change to be recognized / processed someone has to call $apply (or $digestfor that matter). That's it. This is supposed be done by directives and services. It's actually quite hard to get a '$apply already in progress' error if you have done everything right.
My ASP.NET MVC page uses JavaScript/jQuery to poll my database every second.
This is working but I want to make sure that, if there is a delay, my timer handler won't get called again before it has returned.
In there any trick to this other than storing the timer ID in a global variable, clearing the timer in my handler, and restarting it when my handler is done.
NOTE: I realize every second seems frequent but this code is polling my server after submitting a credit card payment. Normally, it will only run for a second or so, and I don't want the user to wait any longer than necessary.
Polling every second? That's quite heavy!
That aside, you won't have this issue when setTimeout is used instead of setInterval. The latter ensures that a piece of code is run x times given a interval, while the former ensures that there's a delay of at least x milliseconds.
function some_poller() {
$.ajax({
url: '/some_page',
success: function() {
setTimeout(some_poller, 1000);
},
error: function() { // Also retry when the request fails
setTimeout(some_poller, 1000);
}
});
}
// Init somewhere
some_poller();
Not really, although I wouldn't recommend using a global variable. Stick it inside some function.
But are you really sure you need to poll every second? That's an extremely chatty interface.
In my personal experience a "global", (inside of the root function), variable works very well in this instance so that you can control when to clear and restart. If the response is really as quick as you say, this shouldn't cause too much overhead, (clearing/resetting), and will allow to account for these type of situations.
I have a piece of Javascript that checks for a condition (via an AJAX call) every n seconds. If that condition is true, it stops checking. I have implemented it in the following way:
var stopTimer;
var timerId = setInterval(function() {
/* Make Ajax Calls and set stopTimer */
if (stopTimer) {
clearInterval(timerId);
}
}, 10000);
However, I find erratic behaviour: Works sometimes, but at other times, it keeps checking forever. I have checked that (as much as is possible) there is no error in any part of the code.
I am therefore suspecting that calling clearInterval inside a setInterval handler might be the culprit. Is that right? Is it OK to call clearInterval inside a setInterval handler?
Thank you for your attention
It's safe. The issue is probably to do with stopTimer not being set as you expect.
I don't think there will be any issue with your code unless the AJAX function is erroneous. You have to take care of the success and error callbacks of the AJAX function so that there won't be any issue with the loop not being stopped.
Also I think you are constantly polling the server for a response and then doing the appropriate action. You can use Reverse AJAX to do this kind of process.
Make sure you're not inadvertently re-using the same timer name elsewhere in your code which would result in you always stopping the second timer to be defined.
Either give the timer a unique name, or scope it to a function
var timerForAjax = setInterval(function() {
/* Make Ajax Calls and set stopTimer */
if (stopTimer)
{
clearInterval(timerForAjax);
}
}, 10000);
I was careless enough to call my timer interval and didn't realize I was creating two timers in the same scope both called interval. Blamed iOS8 for about an hour until I realized that that was nothing to do with it.