Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I remove watchers created by a template which was added and removed dynamically using $compile?

Tags:

angularjs

We have this directive that allows developers to specify their own template and data that will be used for the scope that will be bound to that template by using $compile. The template will likely to have angular expressions which will create watchers.

When the element's content that is represented by the template and scope is removed from the DOM tree, how do I remove the watchers that were created with it?

This link proves that watchers stay even when the DOM it represent is removed from the tree.

Clicking the compile button the first time will compile your angular template and attach it to the DOM tree. The second time the button is clicked, it will empty the element where the previous template was added and add the newly compiled template.

index.html

<!DOCTYPE html>
<html ng-app="app">
  <head>
    <script data-require="[email protected]" data-semver="1.7.2" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script data-require="[email protected]" data-semver="1.2.6" src="https://code.angularjs.org/1.2.6/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="smart-table-debug.js"></script>
    <script src="script.js"></script>
  </head>

  <body ng-controller="controller">
    <h1>Compile Demo</h1>

    HTML to compile<br/>
    <textarea id="html" ng-model="html"></textarea>
    <br/>

    Result<br/>
    <div id="result"></div>
    <br/>
    <input type="button" value="compile" ng-click="compile()" />
    <br/>
    Compile time: {{ compileTime }}<br/>
    Watchers count: {{ watchersCount }}<br/>
    Digest count: {{ digestCount }} <br/>
    Total time taken: {{ totalTime }} milliseconds <br/>
  </body>
</html>

script.js

function getAllScopes( rootScope, allScopes ) {
  if( !allScopes ) {
    allScopes = [];
  }
  allScopes.push( rootScope );

  for( var scope = rootScope.$$childHead; scope; scope = scope.$$nextSibling ) {
    getAllScopes( scope, allScopes );
  }

  return allScopes;
}

angular.module( "app", [] )
.controller( "controller", ["$scope", "$compile", "$rootScope", "$timeout", function($scope, $compile, $rootScope, $timeout ){

  var e = angular.element;

  $scope.html = "<div>{{watchersCount}}</div>"
  $scope.watchersCount = 0;
  $scope.digestCount = 0;
  $scope.compileClickStart = performance.now();
  $scope.totalTime = 0;

  /* because we didn't gave watch expression as 1st parameter but a functionn,
   * the function will be called at every end of digest cycle.
   * https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest
   */
  $rootScope.$watch(function() {
    $scope.digestCount += 1;
    $scope.totalTime = performance.now() - $scope.compileClickStart;
  });

  $scope.compile = function(){
    $scope.digestCount = 0;
    $scope.compileClickStart = performance.now();

        var insertTarget = e('#result');
        var targetScope = $scope.$new();

        insertTarget.empty();
        t0 = performance.now();
        var expandedContent = $compile($scope.html)(targetScope);
        t1 = performance.now();
        $scope.compileTime = (t1 - t0) + " milliseconds.";
        insertTarget.append( expandedContent );

        $timeout( function() {
            var allScopes = getAllScopes($rootScope);
            var allWatchers = [];
            for( var i = 0; i < allScopes.length; i++ ) {
              var scope = allScopes[i];
              if( scope.$$watchers) {
                //allWatchers = allWatchers.concat( scope.$$watchers );
              for( var j = 0; j < scope.$$watchers.length; j++ ) {
              allWatchers.push({
                            "scope" : scope,
                            "watcher" : scope.$$watchers[j] 
                        });
              }
              }
            }
            console.log( allScopes );
            $scope.watchersCount = allWatchers.length;
        });
  };

}]);
like image 807
supertonsky Avatar asked Mar 16 '23 01:03

supertonsky


2 Answers

As mentioned by TGH, the watchers are normally removed when the DOM and scope it lives in is destroyed. If this isn't happening for you, then you can get a reference to the element's scope and destroy the scope manually, removing the watchers.

Irrelevant code is omitted for brevity.

var expandedContent;

$scope.compile = function(){
  // ...

  var insertTarget = e('#result');
  var targetScope = $scope.$new();

  if (expandedContent) {
    expandedContent.scope().$destroy();
  }
  insertTarget.empty();

  expandedContent = $compile($scope.html)(targetScope);
  insertTarget.append( expandedContent );

  // ...
}

If you have already have this functionality in a directive, it's much cleaner when done in its link function. Listen for when the element is removed from the DOM and clean up the scope accordingly.

link: function (scope, element, attrs) {
  element.on("$destroy", function () {
    scope.$destroy();
  })
}
like image 79
user2943490 Avatar answered Apr 25 '23 21:04

user2943490


If the DOM and associated scope is destroyed, the watchers for that content will be gone as well.

like image 39
TGH Avatar answered Apr 25 '23 22:04

TGH