I've got a recursive function which calls another asynchronous function, and upon resolving the promise, calls itself again after a few seconds:
$scope.gamePolling = function () {
if ($scope.getGames) {
$scope.getGameData().then(function () {
$timeout(function () {
$scope.gamePolling();
}, 3000);
});
}
};
When changing the route / state (using ui-router), I thought the $scope should be destroyed so I could turn off the recursive function using:
$scope.$on('destroy', function () {
$scope.getGames = false;
});
However, on the next page the gamePolling function keeps calling itself because the breakpoint inside the destroy never gets hit.
So my question is why isn't the $destroy event being triggered?
p.s. this also happens when removing the $timeout, so the problem must be with the recursion.
I've gotten around this problem, by turning off gamePolling() in the $stateChangeStart:
$scope.$on('$stateChangeStart', function () {
$scope.getGames = false;
});
So the polling stops but the $destroy event still doesn't seem to be triggered.
As a little test, in state/controller A I assigned the current $scope to a $rootScope variable so I could check if it was destroyed in the state/controller B: $rootScope.testScope = $scope;
When checking $rootScope.testScope.$$destroyed in controller B, it returned true. So it looks like the $scope of the controller A was successfully destroyed. However, in $rootScope.testScope I can still access the variables that were assigned to $scope.
It is "$destroy" event, not "destroy"
It is funny because you mention the event with the right name and in the code you are missing the $ sign prefix.
Hope it helps!
UPDATE: If you allow me I suggest you use an interval instead of a timeout+recursive function. You can then "kill" the interval in the $destroy event handler.
Related
How to stop $watch while changing the object
Here is a $watch function
$scope.$watch($scope.OneTime,function(old,new)
{
// my function
});
The above $watch function will be fire whenever my (OneTime) object value has been changed.
But I won't to watch the object on every change, I just want to fire the $watch function when I change the my object on first time only.
I also tried something and find out a function from angular.js script file But I don't know what the below function doing exactly.
You can find this function from angular.js script file
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch, lastValue;
return unwatch = scope.$watch(function oneTimeWatch(scope) {
return parsedExpression(scope);
}, function oneTimeListener(value, old, scope) {
lastValue = value;
if (isFunction(listener)) {
listener.apply(this, arguments);
}
if (isDefined(value)) {
scope.$$postDigest(function () {
if (isDefined(lastValue)) {
unwatch();
}
});
}
}, objectEquality);
}
But am seeing a pretty word unwatch();inside the function . So i think I need to use $unwatch for the object when end of the $watch function. But I couldn't get anything about $unwatch concept anywhere in angular document. but I can see it on angular script.
I had some idea about manually stop this $watch function by this below way
var unwatch = $scope.$watch("OneTime", function() {
//...
});
setTimeout(function() {
unwatch();
}, 1000);
But I am thinking about if angular provide to unwatch function to stop the abject watching, it would be easy to handle in my whole application. So planed to take override something in angular.js file in my application. let me know if you have any idea about override angular.js script file to create $unwatch function as same as $watch function. And also let me know angular had any$unwatch function.
I think you need one way binding over here
you can achieve this br
{{::oneTime}}
in your html page One-time expressions will stop recalculating once they are stable, which happens after the first digest
var $unwatch=$scope.$watch('onetime',function(){
unregister();
}
AngularJS does already provide such function, exactly as you mentioned above. When you create a watcher, it returns you a function that may be used to stop watching it.
From the $rootScope.Scope documentation,
$watch(watchExpression, listener, [objectEquality]);
Returns: function() Returns a deregistration function for this listener.
The only thing you need to do to unwatch your object would be calling the returned function. You could call it inside your watch function so it will be executed at the first time your watcher is invoked.
var unwatch = null;
// start watching the object
var unwatch = $scope.$watch($scope.OneTime, function(old, new)
{
// my function
if (unwatch != null) {
unwatch();
}
});
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.
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.
I've got the following code where I'm trying to show/hide a text box based on if a key was pressed in the global window scope. However, every time a key is pressed, it does not seem to fire the watch service. Why is this?
Plnkr here http://plnkr.co/edit/qL9ShNKegqJfnyMvichk
app.controller('MainCtrl', function($scope, $window) {
$scope.name = '';
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name)
})
$scope.$watch('name', function() {
console.log('hey, name has changed!');
});
});
It is because you are handling the keypress event outside of the digest cycle. I would strongly encourage you to let angular do its thing with databinding or using ngKeypress
Otherwise, in your handler, call $scope.$digest().
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name);
$scope.$digest();
})
On a high level view, watching a value on a scope needs two parts:
First: the watcher - like you created one. Every watcher has two parts, the watch function (or like here the value) and the listener function. The watch function returns the watched object, the listener function is called when the object has changed.
Second: the $digest cycle. The $digest loops over all watchers on a scope, calls the watch function, compares the returned newValue with the oldValue and calls the corresponding listener function if these two do not match. This is called dirty-checking.
But someone has to kick the $digest. Angular does it inside its directives for you, so you don't care. Also all build-in services start the digest. But if you change the object outside of angular's control you have to call $digest yourself, or the preferred way, use $apply.
$scope.$apply(function(newName) {
$scope.name = newName;
});
$apply first evaluates the function and then starts the $digest.
In your special case, I would suggest to use ngKeypress to do it the angular way.
Currently working on a project where we found huge memory leaks when not clearing broadcast subscriptions off destroyed scopes. The following code has fixed this:
var onFooEventBroadcast = $rootScope.$on('fooEvent', doSomething);
scope.$on('$destroy', function() {
//remove the broadcast subscription when scope is destroyed
onFooEventBroadcast();
});
Should this practice also be used for watches? Code example below:
var onFooChanged = scope.$watch('foo', doSomething);
scope.$on('$destroy', function() {
//stop watching when scope is destroyed
onFooChanged();
});
No, you don't need to remove $$watchers, since they will effectively get removed once the scope is destroyed.
From Angular's source code (v1.2.21), Scope's $destroy method:
$destroy: function() {
...
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
...
this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
...
So, the $$watchers array is emptied (and the scope is removed from the scope hierarchy).
Removing the watcher from the array is all the unregister function does anyway:
$watch: function(watchExp, listener, objectEquality) {
...
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
So, there is no point in unregistering the $$watchers "manually".
You should still unregister event listeners though (as you correctly mention in your post) !
NOTE:
You only need to unregister listeners registered on other scopes. There is no need to unregister listeners registered on the scope that is being destroyed.
E.g.:
// You MUST unregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
// You DON'T HAVE to unregister this
$scope.$on(...)
(Thx to #John for pointing it out)
Also, make sure you unregister any event listeners from elements that outlive the scope being destroyed. E.g. if you have a directive register a listener on the parent node or on <body>, then you must unregister them too.
Again, you don't have to remove a listener registered on the element being destroyed.
Kind of unrelated to the original question, but now there is also a $destroyed event dispatched on the element being destroyed, so you can hook into that as well (if it's appropriate for your usecase):
link: function postLink(scope, elem) {
doStuff();
elem.on('$destroy', cleanUp);
}
I would like to add too #gkalpak's answer as it lead me in the right direction..
The application I was working on created a memory leak by replacing directives whom had watches. The directives were replaced using jQuery and then complied.
To fix i added the following link function
link: function (scope, elem, attrs) {
elem.on('$destroy', function () {
scope.$destroy();
});
}
it uses the element destroy event to in turn destroy the scope.
The following is example code from the official firebase documentation
var app = angular.module("myChatRoom", []);
app.factory("ChatService", function() {
var ref = new Firebase("https://<your-firebase>.firebaseio.com/chat");
return {
getMessages: function() {
var messages = [];
ref.on("child_added", function(snapshot) {
messages.push(snapshot.val());
});
return messages;
},
addMessage: function(message) {
ref.push(message);
}
}
});
app.controller("ChatCtrl", ["$scope", "ChatService",
function($scope, service) {
$scope.user = "Guest " + Math.round(Math.random()*101);
$scope.messages = service.getMessages();
$scope.addMessage = function() {
service.addMessage({from: $scope.user, content: $scope.message});
$scope.message = "";
};
}
]);
I would like to understand what is going on here, as I can't assuredly see why this is actually working.
getMessages() is called only once, when the controller fires. Nevertheless the event bindings will fire even when getMessages() is never called afterwards. As well, the $scope property is updated.
Why is that? Also, is that the recommended way of using firebase with an Angular service/factory?
In getMessages you have the following call:
ref.on("child_added", function(snapshot) {
...
This registers a callback that gets called whenever a child is added to your Firebase chat.
Even when getMessages exits, that callback will remain registered. So that is why you only have to call getMessages once. The name is a bit counter-intuitive, I would probably call it something like registerMessageHandler or registerForNewMessages.
Either way: the callback will be triggered for every added child, until you either turn it off (by calling ref.off("child_added") or until the page reloads. This is known as an asynchronous operation: the callback will happen, regardless of where the original flow is. The flow of the callback and your main code are asynchronous. It is in that sense similar to a regular JavaScript setInterval (which also keeps firing asynchronously, while your main code continues) or a common XMLHttpRequest (which will normally fire after its calling method returns).