How The $destroy Event Affects The Scope Tree In AngularJS
The other day, I realized that I had a misunderstanding about how the Scope tree was altered during a $destroy event in AngularJS. Originally, I had thought that after the "$destroy" event was broadcast, each scope was detached from its parent. However, after some further investigation, I now realize that only the root of the subtree is detached and the rest of the destroyed descendant remain intact.
Run this demo in my JavaScript Demos project on GitHub.
Demonstrating this is quite easy. All we have to do is set up a tree of nested scopes, hook into each "$destroy" event, and then remove one of the ancestor scopes. Since the ngIf directive creates a new scope, we can sprinkle some ngIf directives alongside some logging directives to see this in action.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
How The $destroy Event Affects The Scope Tree In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"><link>
</head>
<body ng-controller="AppController">
<h1>
How The $destroy Event Affects The Scope Tree In AngularJS
</h1>
<p>
<a ng-click="toggleContainer()">Toggle Container(s)</a>
</p>
<!-- NOTE: We are using ngIf here to ensure multiple scope are created. -->
<div ng-if="isShowingContainer" bn-logger>
<div ng-if="true" bn-logger>
<div ng-if="true" bn-logger>
Inner inner inner thing.
</div>
</div>
</div>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
angular.module( "Demo" ).controller(
"AppController",
function AppController( $scope ) {
// I determine if we are showing the container.
$scope.isShowingContainer = true;
// ---
// PUBLIC METHODS.
// ---
// I toggle the existence of the container.
$scope.toggleContainer = function() {
$scope.isShowingContainer = ! $scope.isShowingContainer;
};
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I log the scope-parent relationship in and around the $destroy event.
angular.module( "Demo" ).directive(
"bnLogger",
function bnLogger( $timeout ) {
// Return the directive configuration object.
return({
link: link,
restrict: "A"
});
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes ) {
// When the $destroy event is triggered, we want to see how it affects
// the relationship between the current scope and its parent scope.
scope.$on(
"$destroy",
function handleDestroy() {
logIDs( "Before Timeout" );
// Since the "$destroy" event is triggered before the the
// scope tree is altered, we need to wait a tick and then
// check the parent structure again.
$timeout( logIDs, 0, false );
}
);
// I log the IDs of the current scope and parent scope.
function logIDs( prefix ) {
// What we'll see here is that the $parent is NULL for the root
// of the scope sub-tree that was destroyed; but, for all the
// non-root nodes, the $parent scope will be in tact.
// --
// CAUTION: The $$destroyed property is a proprietary AngularJS
// property; it is not meant to be referenced outside of the
// framework. I am just logging it here for clarity.
console.log(
"%s: Scope [%s] with parent scope [%s] .. $$destroyed [%s].",
( prefix || "After Timeout" ),
scope.$id,
( scope.$parent && scope.$parent.$id ),
scope.$$destroyed
);
}
}
}
);
</script>
</body>
</html>
As you can see, we're logging out the scope/parent relationship in each destroyed scope both during and directly after the "$destroy" event. And, when we run this code, we get the following console output:
Before Timeout: Scope [3] with parent scope [2] .. $$destroyed [false].
Before Timeout: Scope [4] with parent scope [3] .. $$destroyed [false].
Before Timeout: Scope [5] with parent scope [4] .. $$destroyed [false].
After Timeout: Scope [3] with parent scope [null] .. $$destroyed [true].
After Timeout: Scope [4] with parent scope [3] .. $$destroyed [true].
After Timeout: Scope [5] with parent scope [4] .. $$destroyed [true].
As you can see, during the "$destroy" event, the entire scope tree is still intact; however, after the "$destroy" event has finished propagating, the $parent scope of the subtree is nullified while each of the nested scopes retains a connection to its destroyed parent scope.
It's quite possible that you will never think about this again. The only reason that I am curious about this behavior is because I was looking for a way to handle pending AJAX requests that were resolved after the originating scope had been destroyed. Personally, I think it would nice if the proprietary "$$destroyed" flag was just made public (ie, "$destroyed"). Then, we could easily leverage it to manage control flow post-destruction. For now, however, I have just fallen back to setting a flag in the "$destroy" event handler.
Want to use code from this post? Check out the license.
Reader Comments
Concise, clean, clear... As always. Thanks for your posts, Ben.
Could this behaviour provoke a memory leak?
Just with your code, those references that keep alive in the subtree, will be anyway removed by the Garbage Collector, right?
Ok, I can't seem to find clear documentation on when a controller's $scope is destroyed. Is it when a controller is destroyed? Is it when a view changes? Do you need to refresh the page?