Angular JS scope leak with element.empty()

Viewed 1504

In an Angular.JS project I have a directive with a complex template, instantiated thousands of times (in a grid).

For performance reasons, I had to split the template in multiple sub templates. Depending on directive's scope values combinations, I choose a sub template.

The performance gain is real (> 100 on very big grids), but in some cases (when the sub template contains a ng-repeat directive) Angular is calling functions declared in scope of deleted DOM elements.

I reduced the problem to the following minimal code:

angular.module('myApp', []).directive('myDirective', ['$compile', '$log', function ($compile, $log) {
  function display(scopeId, x) {
    $log.log('func called with', x, 'in scope', scopeId);

    return x;
  }

  return {
    scope: true,
    link: function (scope, element) {
      scope.array = [1, 2, 3];
      scope.display = display;

      scope.$watch(function () {
        return scope.abc;
      }, function (newValue) {
        var newContent = angular.element('<div ng-repeat="item in array">{{display($id, item)}}</div>');

        element.empty();

        element.append($compile(newContent)(scope))
      });
    }
  };
}]);

You can test it here: https://jsfiddle.net/yss4yskm/6/

Each time the element is replaced (when you click on checkbox), each scope created by ng-repeat is leaked: the scope continue to log to the browser console, Batarang shows the leaked scopes too.

Should not element.empty() delete all the children scopes?

Why the problem only occurs with the ng-repeat directive?

I can avoid the leak by creating a child scope and destroying when needed (https://jsfiddle.net/yss4yskm/9/) but I shouldn't have to.

angular.module('myApp', []).directive('myDirective', ['$compile', '$log', function ($compile, $log) {
  var childScope;

  function display(scopeId, x) {
    $log.log('func called with', x, 'in scope', scopeId);

    return x;
  }

  return {
    scope: true,
    link: function (scope, element) {
      scope.array = [1, 2, 3];
      scope.display = display;

      scope.$watch(function () {
        return scope.abc;
      }, function (newValue) {
        element.empty();

        if (childScope) {
            childScope.$destroy();
        }

        element.append(angular.element('<div ng-repeat="item in array">{{display($id, item)}}</div>'));

        childScope = scope.$new();

        $compile(element.contents())(childScope);
      });
    }
  };
}]);

Thank you.

1 Answers
Related