Removing an ng-repeat without leaks - javascript

I appear to have a memory leak in an Angular based app I've been looking at and I've reduced it to this test case: http://plnkr.co/edit/JIt8PrV28lKnlSx8I8xP?p=preview .
Basically it comes down to the fact that I don't know how to cleanly remove the ng-repeat from the document. If I use jQuery().remove() the table is gone from the document, but cached listeners ($watchCollectionAction() for example) cause a leak.
I've got a $scope.$destroy() call where I want to clean up in the example at the moment, but that isn't the answer...
Thanks!

The plunkr example works if the timeout function is
setTimeout( function () {
vm.persons = [];
$scope.$apply();
}, 1000 );
The $apply is needed since you made a change from outside the Angular machinery (see https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply).

Related

Angular 1.x - What's going on with the order of $scope?

I have a controller where I need to load content using ajax. While it's loading, I'd like a spinner to appear in the interim. The code looks something like the below:
<i class="fa fa-2x fa-spin fa-spinner" ng-show="isLoadingContent"></i>
And the corresponding js:
$scope.isLoadingContent = true;
$q.all(promises).then(function (values) {
$scope.isLoadingContent = false;
// more code - display returned data
However, the UI the spinner does not appear where/when I expect it to appear when I step through the code.
$scope.isLoadingContent = true;
debugger; // the spinner does not appear on the UI
$q.all(promises).then(function (values) {
debugger; // the spinner finally does appear in the UI at this point
$scope.isLoadingContent = false;
// more code - display returned data
I have tried stepping through the code but came up short as to what's going on --
and I am sure I am misunderstanding the sequence of events happening in the Event Loop and where the angular-cycle plays it's role in all of this.
Is someone able to provide an explanation as to why the spinner is set to appear within the promise's method rather than where I set $scope.isLoadingContent? Is it not actually getting set but rather getting queue'd up in the event-loop's message-queue?
------------ EDIT ------------
I believe I came across an explanation as to what's going on. Thanks in large part to, #jcford and #istrupin.
So a little tidbit missing in the original post, the event firing the promise calls and the spinner update was actually based around a $scope.$on("some-name", function(){...}) event - effectively a click-event that is triggered outside of my current controller's scope. I believe this means the $digest cycle doesn't work as it typically does because of where the event-origination is fired off. So any update in the $on function doesn't call $apply/$digest like it normally does, meaning I have to specifically make that $digest call.
Oddly enough, I realize now that within the $q.all(), it must call $apply since, when debugging, I saw the DOM changes that I had expected. Fwiw.
tl;dr - call $digest.
A combination of both answers will do the trick here. Use
$scope.$evalAsync()
This will combine scope apply with timeout in a nice way. The code within the $evalAsync will either be included in the current digest OR wait until the current digest is over and start a new digest with your changes.
i.e.
$q.all(promises).then(function (values) {
$scope.$evalAsync($scope.isLoadingContent = false);
});
Try adding $scope.$apply() after assigning $scope.isLoadingContent = true to force the digest. There might be something in the rest of your code keeping it from applying immediately.
As pointed out in a number of comments, this is absolutely a hack and is not the best way to go about solving the issue. That said, if this does work, you at least know that your binding is set up correctly, which will allow you to debug further. Since you mentioned it did, the next step would then be to see what's screwing up the normal digest cycle -- for example triggering outside of angular, as suggested by user JC Ford.
I usually use isContentLoaded (as oposite to isLoading). I leave it undefined at first so ng-show="!isContentLoaded" is guaranteed to show up at first template iteration.
When all is loaded i set isContentLoaded to true.
To debug your template you need to use $timeout
$timeout(function () { debugger; })
That will stop the code execution right after first digest cycle with all the $scope variable values reflected in the DOM.

Why are these two updates no reflected in the view?

I'm trying to understand how to properly manipulate properties via a controller. The following code executes six updates over four seconds. Updates two and three are not reflected in the view. Why is this, and what do I need to do to have updates of those types affect the view?
Html
<div ng-controller="Controller">
myValue: <span ng-bind="myValue"></span>
</div>
Javascript
var app = angular.module('myApp', []);
app.controller('Controller', function ($scope, $interval) {
$scope.myValue = "first";
console.log($scope.myValue);
setTimeout(function() {
$scope.myValue = "second"; // never updates
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // never updates
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);
$interval(function() {
$scope.myValue = "fifth";
console.log($scope.myValue);
$interval(function() {
$scope.$emit("my-event", "sixth");
}, 1000, 1);
}, 3000, 1);
$scope.$on('my-event', function (event, arg) {
$scope.myValue = arg;
console.log(arg);
});
});
JSFiddle
Use $timeout instead of setTimeout to opt-in to the digest cycle. second won't show since the turn of the digest cycle overrides the value of myValue.
Updated fiddle: https://jsfiddle.net/d9gbpddy/4/
You can try {{myValue}} instead of a <span> element
So I obviously wasn't clear enough in the original question, as the upvoted answer (correctly) suggests using $timeout rather than setTimeout, however the original intent was to understand why the updates were not being reflected in the view, and what could be done to have these types of updates (that originate outside angular) affect the view as was intended.
Read the Scope guide
So whilst I chose to skip the Scopes section of the developer guide because it looked to be the most boring, it was probably the most important, and it clearly points out some items imperative to understanding how angular binds data, notably the Scope Life Cycle which notes;
When the browser calls into JavaScript the code executes outside the
Angular execution context, which means that Angular is unaware of
model modifications. To properly process model modifications the
execution has to enter the Angular execution context using the $apply
method. Only model modifications which execute inside the $apply
method will be properly accounted for by Angular.
There's an excellent answer here that further explains this concept. The first setence aptly reiterates the importance of understanding scope:
You need to be aware about how Angular works in order to understand
it.
Don't just call $scope.$apply
So you start adding calls to $scope.$apply around the place to cater for these things that originate outside angular, but then eventually you start getting:
Error: $digest already in progress
Which means you can't call $scope.$apply whilst $digest is executing. After which you may think, well how can I conditionally call $scope.$apply based on whether the $digest is currently running. But, you don't need to do that...
Just use $timeout
Hah, like the upvoted answer, I know, but based on a different thought process I think. See this answer. $timeout is not just being used in place of setTimeout, but rather is being used (without a delay) to wrap any model updates that are called from outside the Scope Life Cycle, and doing so ensures no conflict with any currently processing $digest.
Wrapping up
So, in the original code snippet, the second and third updates are not reflected in the view because they are performed outside the Angular execution context. The fact that third update doesn't affect the model also means that calling events outside the execution context doesn't get you into the execution context either.
The fourth update is already wrapped inside $interval, which itself causes the updates that code to be run on the next digest. Therefore, updating the code to show an example of an event outside the angular execution context that causes its updates to be shown in the view is as follows:
setTimeout(function() {
$timeout(function({ // start wrap
$scope.myValue = "second"; // now this updates!
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // now this updates!
})); // end wrap
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);

Should I remove Angular watches?

I'm developing a search engine inside angular view and I was wondering if search engine is used too many times and the user does not leave the search engine's view, how should I do to avoid the problem of overloading scope?
When I carry out a lot of searches, I can notice the view is slower. I think this problem is caused by scope overload, but I'm not sure.
An example:
If I get the results of a request into $scope.variable1 and after I make another request again and overwrite $scope.variable1...what happen? are watchers of data structure inside old variable1 removed automatically?
In summary, sometimes when I use too many times an angular views without leaving it the view is slowed down. Which is the best practice to deal with it?
Anytime a scope variable changes a digest cycle is triggered meaning all watchers are checked to see if anything has changed. ( actually 2 times for dirty checking ). In a search field you should throttle how many times you update your scope variable otherwise the digest cycle will kick in too much. you can do this with
debounce
<input type="text" name="variable1"
ng-model="variable1"
ng-model-options="{ debounce: 1000 }" />
Also make sure you are not creating a new watcher when $scope.variable1 changes. declare the watcher once in your controller
Anytime you use:
{{variable}}
you are implicitely creating a watcher on that page.
Whenever your page contains more than 2000 watchers you will see slowing of the page because the digest cycle will take to long for it to be snappy
you can use this snippet to count the number of watchers on your page :
(function () {
var root = angular.element(document.getElementsByTagName('body'));
var watchers = [];
var f = function (element) {
angular.forEach(['$scope', '$isolateScope'], function (scopeProperty) {
if (element.data() && element.data().hasOwnProperty(scopeProperty)) {
angular.forEach(element.data()[scopeProperty].$$watchers, function (watcher) {
watchers.push(watcher);
});
}
});
angular.forEach(element.children(), function (childElement) {
f(angular.element(childElement));
});
};
f(root);
// Remove duplicate watchers
var watchersWithoutDuplicates = [];
angular.forEach(watchers, function(item) {
if(watchersWithoutDuplicates.indexOf(item) < 0) {
watchersWithoutDuplicates.push(item);
}
});
console.log(watchersWithoutDuplicates.length);
})();
if to many watches exist consider using:
{{::variable}}
this will create a one time binding and eliminate some watchers on your page.
Another tip is to use pagination for your search results, this will also limit the amount of watchers on your page
and lastely you probably shouldnt use watchers to begin with.
read this article:
probably dont need watchers
Kind regards,

How to impact the HTML rendering priorities with AngularJS?

I'm prerendering my HTML pages for the search engines bots via PhantomJS through Selenium, so that they can see the fully loaded content. Currently, after PhantomJS reached the page, I'm waiting 5 seconds so that I'm sure everything is loaded.
Instead of waiting those 5 seconds every time, one solution I contemplate is to wait until an attribute html-ready on the <body /> tag is set to true:
<html ng-app>
<head>...</head>
<body html-ready="{{htmlReady}}">
...
</body>
</html>
.controller("AnyController", function($scope, $rootScope, AnyService) {
$rootScope.htmlReady = false;
AnyService.anyLongAction(function(anyData) {
$scope.anyData = anyData;
$rootScope.htmlReady = true;
});
})
The question is: will the html-ready attribute always be set to true after any view update has been done (e.g. displaying the anyData)? In other words, is it possible that during a laps, the html-ready attribute is true while the page is not fully loaded yet? If yes, how can it be handled?
It should be done after the digest, thus it has more chances to work as expected.
AnyService.anyLongAction(function(anyData) {
$scope.anyData = anyData;
$timeout(function () {
$rootScope.htmlReady = true;
}, 0, false);
});
But it is useless in terms of the app. You have to watch for changes in every single place, Angular doesn't offer anything to make the task easier.
Fortunately, you are free to abstract from Angular and keep it simple.
var ignoredElements = [];
ignoredElements = ignoredElements.concat($('.continuously-updating-widget').toArray());
var delay = 200; // add to taste
var timeout;
var ready = function () {
$('body').off('DOMSubtreeModified');
clearTimeout(timeoutLimit);
alert('ready');
};
$('body').on('DOMSubtreeModified', function (e) {
if (ignoredElements.indexOf(e.target) < 0) {
clearTimeout(timeout);
timeout = setTimeout(ready, delay);
}
});
var timeoutLimit = setTimeout(ready, 5000);
Feel free to angularify it if needed, though it isn't the production code anyway.
It is a good idea to put the handler into throttle wrapper function (the event will spam all the way). If you use remote requests on the page that can potentially exceed timeout delay, it may be better to combine this approach with several promises from async services and resolve them with $q.all. Still, much better than looking after every single directive and service.
DOMSubtreeModified is considered to be obsolete (it never was really acknowledged, MutationObserver is recommended instead), but current versions of FF and Chrome support it, and it should be ok for Selenium.
Short answer
No. It isn't guaranteed that your markup will be completely rendered when html-ready is set.
Long answer
To the best of my knowledge it's not possible to accurately determine when Angular has finished updating the DOM after the model changed. In general it happens very fast and it doesn't take more than a few cycles to finish, but that's not always the case.
Correctly detecting when a page has finished loading/rendering is actually quite a challenge, and if you take a look at the source code of specialized tools, like prerender, you'll see that they use several different checks in order to try to decide whether a page is ready or not. And even so it doesn't work 100% of the time (Phantom may crash, a request may take longer than usual to complete, and so on).
If you really want to come up with your own solution for this problem, I suggest that you take a look at prerender's source code (or another similar project) to get some inspiration.

Make AngularJS skip running a digest loop if $http.get() resulted in no new data

I'm currently polling the server to check for new data, and then update the model in an AngularJS app accordingly. He're roughly what I'm doing:
setInterval(function () {
$http.get('data.json').then(function (result) {
if (result.data.length > 0) {
// if data, update model here
} else {
// nothing has changed, but AngularJS will still start the digest cycle
}
});
}, 5000);
This works fine, but most of the requests will not result in any new data or data changes, but the $http service doesn't really know/care and will still trigger a digest cycle. I feel this is unnecessary (since the digest cycle is one of the heaviest operations in the app). Is there any way to still be able to use $http but somehow skip the digest if nothing has changed?
One solution would be to not use $http but jQuery instead, and then call $apply to let Angular know that the model has changed:
setInterval(function () {
$.get('data.json', function (dataList) {
if (dataList.length > 0) {
// if data, update model
$scope.value = dataList[0].value + ' ' + new Date();
// notify angular manually that the model has changed.
$rootScope.$apply();
}
});
}, 5000);
While this seems to work, I'm not sure it's a good idea. I would still like to use pure Angular if possible.
Anyone got any suggestions for improvements to the approach above or a more elegant solution entirely?
P.S. The reason I'm using setInterval instead of $timeout is because $timeout would also trigger a digest cycle which would be unnecessary in this case and only add to the "problem".
Solution provided by AngularJS #doc
AngularJS recommends to use a PERF trick that would bundle up a few $http responses in one $digest via $httpProvider. This again, is not fixing the problem, it's just a sedative :)
$httpProvider.useApplyAsync(true)
Saving the $$watchers solution - risky and not scalable
Firstly, the accepted solution is not scalable - there's no way you're going to do that $watchers trick on a 100K lines of JS code project - it's out of the question.
Secondly, even if the project is small, it's quite risky! What happens for instance if another ajax call arrives that actually needs those watchers?
Another (feasible) risky solution
The only alternative to achieve this without modifying AngularJS code would be to set the $rootScope.$$phase to true or '$digest', make the $http call, and set back the $rootScope.$$phase to null.
$rootScope.$$phase = true;
$http({...})
.then(successcb, failurecb)
.finally(function () {
$rootScope.$$phase = null;
});
Risks:
1) other ajax calls might try to do the same thing --> they need to be synchronized via a wrapping ajax service (over $http)
2) user can trigger UI actions in between, actions that will change the $$phase to null and when the ajax call will come back, and still trigger the $digest
The solution popped after scanning AngularJS source code - here's the line that saves the situation: https://github.com/angular/angular.js/blob/e5e0884eaf2a37e4588d917e008e45f5b3ed4479/src/ng/http.js#L1272
The ideal solution
Because this is a problem that everyone is facing with AngularJS, I think it needs to be addressed systematically. The answers above are not fixing the problem, are only trying to avoid it.
So we should create a AngularJS pull request that would allow us to specify via $httpProvider a config that would not trigger a digest for a specific $http request. Hopefully they agree that this needs to be addressed somehow.
Web sockets would seem to be the most elegant solution here. That way you don't need to poll the server. The server can tell your app when data or anything has changed.
You can do it by this trick :
var watchers;
scope.$on('suspend', function () {
watchers = scope.$$watchers;
scope.$$watchers = [];
});
scope.$on('resume', function () {
scope.$$watchers = watchers;
watchers = null;
});
With this you will remove your scope or reinsert it on the $digest cycle.
You have to manage events to do that of course.
Refer to this post :
Remove and restore Scope from digest cycles
Hope it helps !

Categories

Resources