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.
Related
I had to add a $destroy event listener to the element object in a directive from what I found with this answer Why isn't $destroy triggered when I call element.remove?
Resulting in a link function made with a scope/element....
controller: "MyCtrl",
link: function(scope, element) {
element.on("$destroy", function() {
scope.func();
});
}
Where func is a function defined in MyCtrl.
This works for what I want...but I'm having trouble testing the element.on("$destroy" event.
After injecting/mocking in my directive test, I create the element such like...
this.$compile = $injector.get("$compile");
this.$rootScope = $injector.get("$rootScope");
this.$scope = this.$rootScope.$new();
this.template = "<my-dir></my-dir>";
this.initElement = function() {
this.element = this.$compile(this.template)(this.$scope);
return this.element;
};
Trying to write a unit test, with destroying the scope. The element destroy event isn't triggered...and my this.element does not have a $destroy function it to call. So I'm not sure exactly how I trigger the element's $destroy event.
it("when element destroyed, call scope.func", function() {
this.$httpBackend.whenGET("app/my-dir.tpl.html").respond(200);
this.$scope.unsubscribeToMapMoveEvents = jasmine.createSpy("func");
this.initElement();
this.$scope.$destroy();
expect(this.$scope.func).toHaveBeenCalled();
});
I think the problem I'm facing in this unit test is the same reason why I moved this logic from the ctrl to the directives link function
Any help on how I can test this element on destroy workflow?
My solution was the directive that is defined using this controller is using a $rootScope passed in, and that was breaking it to where the destroy wouldn't kick off.
I instead changed the directive to be initialized with it's own empty scope like....
controller: "MyCtrl",
scope: {}
Can someone please provide an example of scope's $destroy event? Here is the reference documentation from http://docs.angularjs.org/api/ng.$rootScope.Scope#$destroy
$destroy()
Removes the current scope (and all of its children) from the parent
scope. Removal implies that calls to $digest() will no longer
propagate to the current scope and its children. Removal also implies
that the current scope is eligible for garbage collection.
The $destroy() is usually used by directives such as ngRepeat for
managing the unrolling of the loop.
Just before a scope is destroyed a $destroy event is broadcasted on
this scope. Application code can register a $destroy event handler
that will give it chance to perform any necessary cleanup.
Demo: http://jsfiddle.net/sunnycpp/u4vjR/2/
Here I have created handle-destroy directive.
ctrl.directive('handleDestroy', function() {
return function(scope, tElement, attributes) {
scope.$on('$destroy', function() {
alert("In destroy of:" + scope.todo.text);
});
};
});
$destroy can refer to 2 things: method and event
1. method - $scope.$destroy
.directive("colorTag", function(){
return {
restrict: "A",
scope: {
value: "=colorTag"
},
link: function (scope, element, attrs) {
var colors = new App.Colors();
element.css("background-color", stringToColor(scope.value));
element.css("color", contrastColor(scope.value));
// Destroy scope, because it's no longer needed.
scope.$destroy();
}
};
})
2. event - $scope.$on("$destroy")
See #SunnyShah's answer.
I have a directive which does something like this in its link function
angular.module('myApp')
.directive('barFoo', function() {
return {
restrict: 'E',
link: function (scope, element) {
element.on('click', ....);
}
};
});
Now I would like to verify in my unit test that it calls on correctly
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
spyOn(element, 'on');
...
expect(element.on).toHaveBeenCalled();
It tells me the spy is not called. From what I've found on the web, angular/jquery creates a new wrapper around the DOM element every time. Meaning the the element inside my directive is not the same as the element in my spec file. Most likely (not verified) the element[0] probably are the same. I've also tried to spy on angular.element
var mockEl = { on: angular.noop };
spyOn(angular, 'element').andReturn(mockEl);
spyOn(mockEl, 'on');
but that seems to break more than it fixes (I also need functions like isloateScope for example).
Anyway, is there some easy way I can spy on the on function of the element used inside a directive?
Link function can be tested separately
element = angular.element('<bar-foo');
spyOn(element, 'on');
BarFooDirective[0].link(scope, element);
expect(element.on).toHaveBeenCalled();
if it is simple enough (stays away from attrs, required controllers, dependencies), otherwise the spec will cause more problems than it can solve.
Otherwise, it can be tested like it is done by the framework:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toBeDefined();
For real-world directive which may have more than one click listener defined in either itself or child directives it is not enough to make sure that listeners exist. Generally you may want to encapsulate internal functions, but anonymous click handler can also be exposed to scope for the purpose of testing:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toContain(scope.onClick);
Well, it's not necessary to test on method. You can test event bindings with the help of triggerHandler()
link: function (scope, element) {
element.on('click', 'something to heppen');
}
Test:
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
$(element[0]).triggerHandler('click');
expect('something binded to click event to happen');
If you've compiled your element in in a beforeEach like this:
myEl = '<div my-el>MY EL</div>';
scope = $rootScope.$new();
directiveElement = $compile(myEl)(scope);
scope.$digest();
Try replacing mousedown with the event you are testing like this:
expect(directiveElement.on.mousedown).toBeDefined;
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.
I have a directive that's watching a controller property which is modified within an event handler.
The code looks something like this:
vm = this;
vm.someProperty = false;
// Event listeners
$scope.$on('controller.loaded', function (event, data) {
// data.someProperty === true.
angular.extend(vm, data);
});
I then have a directive that uses this property:
<body mydirective="someController.someProperty">
and an $observe on the property value inside the directive:
attrs.$observe(attrs.mydirective, function (value) {
When I make changes to vm.someproperty from within the event listener, the $observe handler is never triggered. My guess is that the change is outside of an angular scope that would allow it to register the change.
Do I need to trigger something in this case to make sure all observed properties and their dependencies are re calculated?