Ran across a very "down-the-rabbit-hole" Angular question today I couldn't find the answer to. From the $scope docs, you can register an event handler on "$destroy", which is called right before a scope's destruction. That way, you can deregister event handlers like so:
var deregister = $scope.$on('myCustomEvent', function () {
// do some crazy stuff
});
$scope.$on('$destroy', function () {
deregister();
});
However, the $scope.$on('$destroy', ...) must create its own handler. Is that automatically destroyed, or do you have to do something like the following to destroy it?
var deregister = $scope.$on('myCustomEvent', function () {
// do some crazy stuff
});
var deregisterDestroy = $scope.$on('$destroy', function () {
deregister();
deregisterDestroy();
});
The answer is actually "maybe" depending on what you mean by it being automatically destroyed. If we look at the source for the $destroy method for scopes, we can see that while a $destroy event is broadcasted downward throughout child scopes, the actual $destroy method is never invoked on any scope but the initial one. That means that the actual cleanup and nulling out of properties never occurs on child scopes.
The reason that this doesn't leak memory is because once $destroy has been invoked on a scope, it becomes detached from the parent scope and is therefore eligible for garbage collection since it should no longer have any path to the GC Roots. This same logic applies to all child scopes since they also should have no paths to the GC Roots.
Your example is safe though; I do that myself in the same manner to clean up my own handlers when necessary and do not run into any kind of infinite loops.
Related
I am working with ionic 3. So I have this code on controller and it works
$scope.note = 'Lorem...';
$rootScope.$on('Active',function() {
$timeout(function() {
$scope.note = 'test';
},0);
});
But why this not work?
$scope.note = 'Lorem...';
$rootScope.$on('Active',function() {
$scope.note = 'test';
});
What is the best approach for this?
In this context it can be considered a bad practice. Spontaneous use of $timeout usually indicates that a developer doesn't know if the code runs inside or outside of digest cycle and tries to play it safe.
The explantion why this doesn't work stays outside of the scope of posted code, but the reason is that this code runs outside of a digest. This depends on where Active scope event is triggered, and this is what a developer should care about in the first place, since scope events don't necessarily happen inside of digest cycle.
If the event is known to happen outside of a digest, digest-dependent code should be wrapped with $apply:
$scope.$on('Active',function() {
$scope.$apply(function() {
$scope.note = 'test';
});
});
If the event is known to happen both inside and outside of a digest, code should be wrapped with $evalAsync:
$scope.$on('Active',function() {
$scope.$evalAsync(function() {
$scope.note = 'test';
});
});
$timeout(...) is supposed to be used only when it's behaviour is wanted, i.e. one tick delay or more and a digest.
As it was suggested by #georgeawg, it is also a bad practice to use $rootScope as global event bus in controllers (they have access to child scopes). This is basically an antipattern that may cause memory leaks. Considering that an event was $broadcasted, it will propagate to child scopes. As a rule of thumb, it should be $scope.$on(...), unless there are reasons why it should be done on $rootScope specifically.
This happens when the view does not get notified of a change in controller. It usually happens for ng-repeat arrays/objects or inside the forms that do not use a global object. The $timeout solution works or you can call $scope.$apply(). Please look at this document for more information:
https://www.sitepoint.com/understanding-angulars-apply-digest/
What does the following function wrapped by scope.$apply do? I can't seem to find the answer to this, but I see examples where it is used in directives.
scope.$apply(function() {
fn(scope, {
$event: evt
})
});
The closest explanation I could find implies that this might be used when the event that you want to respond to is not handled by Angular directives. Here is the explanation I am referring to.
If someone could provide the intended use of this pattern and what it means, it would be appreciated.
EDIT 1
I should have seen this earlier. Must need more sleep.
Since my example is not a complete working one. It makes a lot more sense after looking at the referenced explanation carefully.
The fn(scope, {$event: evt}) call is invoking the parsed reference to a custom function via the directive parsed in the following line:
var fn = $parse(tAttrs.myContextmenu);
So the target function implementation is capturing a specific event via the directive and then suppressing it.
So I guess this is useful when you do not want to clutter directives with controller specific functions and maybe fire a different event in response to another event then let a controller handle it.
scope.$apply is used to manually trigger Angular's digest cycle for any async events that happen outside of Angular's execution context.
One such async event is element.on("click", function(e){...}) (or any other event related captured with .on), but could also be other async function outside of Angular context.
The second part is an invocation of the "$parsed" expression. It accepts a scope as a parameter and a map of "local" variables, such as {$event: evt}. The intent is every similar to what scope: "&" is doing - but without creating an isolate scope. For example, if the expression is:
<my-directive p="doSomething(foo)">
then, if doSomething(foo) is $parsed, the caller can supply the value of foo:
var parsedFn = $parse(attrs.p);
parsedFn(scope, {foo: 5})'
This will cause the invocation of doSomething(5)
Posting an answer to this since it makes sense to me (see my edit).
The fn(scope, {$event: evt}) call is invoking the parsed reference to a custom function via the directive parsed in the following line:
var fn = $parse(tAttrs.myContextmenu);
So the target function implementation is capturing a specific event via the directive and then suppressing it.
So I guess this is useful when you do not want to clutter directives with controller specific functions and maybe fire a different event in response to another event then let a controller handle it.
I'm broadcasting an event from my navbar controller to another controller, but if I initialize the controller multiple times (when I'm going front and back through the application) the function that executes on my $on event runs multiple times because it's registered multiple times.
$rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
});
How can I prevent the submitBookingDialog() to happen more than once?
I found a solution, but I don't know if it's ideal.
First of all, do you need to send the event on the $rootScope? If not, then you could just register your event handler on the $scope. The event handler will be destroyed whenever your controller scope is destroyed. You would then send the event via $scope.$emit or $scope.$broadcast depending on your controller hierarchy.
That being said, all you need to do to destroy your event listener is call the deregistration function that is returned when registering the listener:
var offSubmitBookingDialog = $rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
});
$scope.$on('$destroy', function() {
// Call the deregistration function when the scope is destroyed
offSubmitBookingDialog();
});
This seems to do it for me:
var removeListener = $rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
// Remove listener
removeListener();
});
For posterity I ended up doing this:
.run(function($rootScope) {
$rootScope.once = function(e, func) {
var unhook = this.$on(e, function() {
unhook();
func.apply(this, arguments);
});
};
})
Because I kept needing to do this in a few places this just ended up being cleaner.
With that on your app module you can now just call once instead of $on:
$rootScope.once('submitBookingDialog', function() {
submitBookingDialog();
});
I ran into a similar situation, so I wrote a small library to make pub/sub stuff easier.
https://github.com/callmehiphop/hey
Maybe you shoud unsubscribe on controller destroy event
var removeSubmitBookingDialog = $rootScope.$on('submitBookingDialog',submitBookingDialog);
$scope.$on("$destroy", removeSubmitBookingDialog);
I have a code that use $scope.$on one time on init and then in a function, so the code is executed multiple times. How can I unbind if first before I bind it again. I've try $scope.$off but there's not such function, https://docs.angularjs.org/api say nothing about $on. I'm using angular 1.0.6.
If you don't un-register the event, you will get a memory leak, as the function you pass to $on will not get cleaned up (as a reference to it still exists). More importantly, any variables that function references in its scope will also be leaked. This will cause your function to get called multiple times if your controller gets created/destroyed multiple times in an application.
Fortunately, AngularJS provides a couple of useful methods to avoid memory leaks and unwanted behavior:
The $on method returns a function which can be called to un-register the event listener.
Whenever a scope gets cleaned up in Angular (i.e. a controller gets destroyed) a $destroy event is fired on that scope. You can register to $scope's $destroy event and call your cleanUpFunc from that.
See the documentation
Sample Code:
angular.module("TestApp")
.controller("TestCtrl",function($scope,$rootScope){
var cleanUpFunc = $scope.$on('testListener', function() {
//write your listener here
});
//code for cleanup
$scope.$on('$destroy', function() {
cleanUpFunc();
};
})
$scope.$on returns a function which you can call to unregister.
I have a chunk of markup in my page that represents a view, and a JS controller function which is associated with that view. (These are Angular, but I don't believe that matters.) The controller code listens for a custom event fired from elsewhere in the app, and handles that event with some controller-specific logic.
My problem is that the controller's event handler is getting attached too many times: it gets attached every time the view is re-activated, resulting in the handler being run multiple times every time the custom event is fired. I only want the handler to run once per event.
I've tried using .off() to unbind the handler before binding it; I've tried .one() to ensure that the handler is only run once; and I've tried $.proxy() after reading about its interaction with .off() here.
Here's a sketch of my code:
// the code inside this controller is re-run every time its associated view is activated
function MyViewController() {
/* SNIP (lots of other controller code) */
function myCustomEventHandler() {
console.log('myCustomEventHandler has run');
// the code inside this handler requires the controller's scope
}
// Three variants of the same misbehaving event attachment logic follow:
// first attempt
$('body').off('myCustomEvent', myCustomEventHandler);
$('body').on('myCustomEvent', myCustomEventHandler);
// second attempt
$('body').one('myCustomEvent', myCustomEventHandler);
// third attempt
$('body').off('myCustomEvent', $.proxy(myCustomEventHandler, this));
$('body').on('myCustomEvent', $.proxy(myCustomEventHandler, this));
// all of these result in too many event attachments
};
// ...meanwhile, elsewhere in the app, this function is run after a certain user action
function MyEventSender() {
$('body').trigger('myCustomEvent');
console.log('myCustomEvent has been triggered');
};
After clicking around in my app and switching to the troublesome view five times, then doing the action which runs MyEventSender, my console will look like this:
myCustomEvent has been triggered
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run
How can I get it to look like this:
myCustomEvent has been triggered
myCustomEventHandler has run
???
Give your events a namespace, then simply remove all events with said namespace when you re-run the controller.
jsbin
$('body').off('.controller');
$('body').on('myCustomEvent.controller', myCustomEventHandler);
You could listen in on the scope destroy event in your Main controller
function MyViewController($scope) {
function myCustomEventHandler() {
console.log('myCustomEventHandler has run');
// the code inside this handler requires the controller's scope
}
$('body').on('myCustomEvent', myCustomEventHandler);
$scope.$on("$destroy", function(){
$('body').off('myCustomEvent', myCustomEventHandler);
//scope destroyed, no longer in ng view
});
}
edit This is an angularJS solution. The ngview is constantly being loaded as you move from page to page. It will attach the event over and over again as the function is repeatedly called. What you want to do is unbind/remove the event when someone leaves the view. You can do this by hooking into a scopes $destroy (with the dollar sign) event. You can read up more on that here: $destroy docs
The problem is that when function MyViewController(){} is called multiple times, you get a separate instance of myCustomEventHandler (attached to the current closure), so passing that to $.off doesn't unregister the previous handler.
KevinB's answer, event namespaces, is what I suggest for removing specific handlers without requiring knowledge of which handler was installed. It'd be nicer if you could unregister the events when the element is removed/hidden, then you would have the reference to the function you want to unregister, without risking removing handlers that other code may have added to the same event namespace. After all, event namespace is just a global pool of string and is susceptible to name collision.
If you make your function global, it will also work (except that it looks like you need the closure), but I'm just showing it to explain the problem, use namespaces
function myCustomEventHandler() {
console.log('myCustomEventHandler has run');
// the code inside this handler requires the controller's scope
}
function MyViewController() {
// first attempt
$('body').off('myCustomEvent', myCustomEventHandler);
$('body').on('myCustomEvent', myCustomEventHandler);
// second attempt
$('body').one('myCustomEvent', myCustomEventHandler);
// third attempt
$('body').off('myCustomEvent', $.proxy(myCustomEventHandler, this));
$('body').on('myCustomEvent', $.proxy(myCustomEventHandler, this));
}
// ...meanwhile, elsewhere in the app, this function is run after a certain user action
function MyEventSender() {
$('body').trigger('myCustomEvent');
console.log('myCustomEvent has been triggered');
}
MyViewController();
MyViewController();
MyEventSender();
Previous Idea
One of the problems is that you're not passing the same function to $.on and $.off, so off is not unregistering anything in this case
Not the problem, leaving the answer up for reference since it's not exactly intuitive. $.proxy seems to return a reference to the same bound function if passed the same function and context. http://jsbin.com/adecul/9/edit