I've discovered a resource leak on a webpage I'm working on.
This webpage has two textfields, that upon click show a modal dialog, perform a data request to the backend, and then present that information in a table that the user can select an entry from for use in the textbox they originally clicked.
I'm binding the click events to the textboxes like so:
var $field = $('#one-of-the-text-fields');
$field.click(function () {
App.DialogClass.show();
App.DialogClass.PopulateTable();
App.DialogClass.GotoPageButtonAction(actionArgs); // Offender!
});
...Which calls...
App.DialogClass = (function($) {
var pub {},
$gotoPage = $('#pageNumberNavigationField'),
$gotoPageButton = $('#pageNumberNavigationButton');
// ...SNIP unimportant other details...
pub.GotoPageButtonAction = function (args) {
$gotoPageButton.click(function () {
var pageNumber = $gotoPage.val();
pub.PopulateTable(args); // Breakpoint inserted here...
});
};
return pub;
})(jQuery);
I noticed the leak because when I ran through using Chrome's JavaScript debugger, I'm always having one extra breakpoint hit every time I click a different button (e.g. the first time I click field A, the breakpoint is hit twice. When I hit field B after that, the break point is hit three times. If I click A after that, the breakpoint is hit four times. Extrapolate as necessary.)
Nowhere in my code am I doing anything about an existing click event for a given field. I suspect my leak stems from the fact that the events are not getting cleaned up. That being said, I am also not terribly familiar with JavaScript/jQuery. What are some techniques for removing click events from a control?
Sure. Just unbind them:
$field.unbind('click');
However, bear in mind that this will remove all event handlers for click, not just yours. For safety, you should use namespaces when binding handlers:
$field.bind('click.mynamespace', function(){
// do something
});
Then,
$field.unbind('click.mynamespace');
So, then, only your handler will be removed.
If you have used .bind() to bind them .unbind() removes the events
If you have used .on() to bind them .off() removes the events
JQuery offers the unbind function to unbind event listeners.
Note that you may also do it in vanilla JS using removeEventListener.
But instead of unbinding, you probably should not bind each time in GotoPageButtonAction : once is enough.
Related
I have a Chrome extension that intercepts and checks tweets before they get posted. To do this, I've add an event listener to the Tweet button. Sine the content is dynamic, I use the solution proposed in this thread:
initialize : function() {
let that = this;
let jsInitChecktimer = setInterval(checkForJsFinished, 111);
function checkForJsFinished () {
if (document.querySelector("div[data-testid='tweetButtonInline']")) {
clearInterval (jsInitChecktimer);
console.log("Button found");
that.addSubmitNewTweetClickHandler();
}
}
},
addSubmitNewTweetClickHandler : function() {
let that = this;
let buttonSubmitTweet = document.querySelector("div[data-testid='tweetButtonInline']");
buttonSubmitTweet.addEventListener('click', function(e) {
console.log("CLICK");
// Stop default event from happening
e.preventDefault();
e.stopImmediatePropagation();
// Do stuff
});
},
If the tweet passed the checks alright, it gets submitted by programmatically triggering the event using .trigger('click').
This works fine, but only once. After a tweet has been submitted and posted, the event listener on the Tweet button is gone, and I cannot intercept the next tweet to check it. I've tried calling initialize() after submitted again -- maybe the button gets removed and newly added to the DOM (it actually disappears fire a moment when submitting a tweet) -- but the querySelector finds the button immediately. But even after calling initialize() again, no click even on the Tweet button fires.
What could be the issue here? My problem is that I don't even know where to look for and how to debug this.
After many more hours, I've finally figured it out. The problem was essentially the highly dynamic content of the new Twitter website. After submitting a tweet, the Tweet button gets indeed removed and added again. In needed to do a serious of changes:
Use a MutationObserver to keep track of any changes. Every time there's a change, call the initialize() function. To avoid too many calls, I do this in case of certain changes (unnecessary detail here)
Change the addSubmitNewTweetClickHandler() method so that the event listener first gets removed in order to avoid duplicate listeners (please note that I use objects hence the use of this compared to my original question)
addSubmitNewTweetClickHandler : function() {
let that = this;
let buttonSubmitTweet = document.querySelector("div[data-testid='tweetButtonInline']");
buttonSubmitTweet.removeEventListener('click', this.handleSubmitNewTweetClick );
this.handleSubmitNewTweetClick = this.handleSubmitNewTweetClick.bind(this)
buttonSubmitTweet.addEventListener('click', this.handleSubmitNewTweetClick );
},
This change required to create the reference function handleSubmitNewTweetClick
Overall, it's still not a perfect solution since I call initialize() many unnecessary time. However, I failed to reliably identify when the Tweet button was added to the document. When I used the MutationObserver none of the added nodes had the attribute data-testid which I need to identify the correct button. I have node idea why this attribute was not there. Maybe the attribute is added some times after added to button, but even with an additional MutationObserver looking for attribute changes I could detect this.
Anyway, it works now and it's only for a prototype.
Problem Fiddle:
Click To View (Attempt #1, using Delegated Events)
Click To View (Attempt #2, brute force approach as below)
Click To View (Attempt #3, refactored, has problem I am trying to solve)
On a project I'm working with, I'm exploring a rather dynamic form. In addition to some static elements, there are various interactive elements, which can be cloned from a hidden 'template' markup set and added at various points in the business process.
Because of the dynamic nature, my tried-and-true method of setting up a jQuery element cache and event handlers on-load, then letting the user do whatever isn't working out, because of this dynamic nature; I was finding that my dynamically-added elements had no click events.
To solve this problem, I manually set up a rebind method for each scripted element in question. The rebind process involves A) re-acquiring the set of elements for a given descriptive selector, B) dropping any existing events on that cache, as those events apply to an incomplete element set, and C) calling a bind method to apply the new events to the entire set.
The brute-force, working way that I got, had this going on:
var $elementCache = $('.some-class');
function rebindSomeLink() {
// Re-acquire the element cache...
$elementCache = $('.some-class');
// Drop all existing events on the cache...
$elementCache.unbind();
// Call a bind function to establish new events.
bindSomeLink();
}
function bindSomeLink() {
$elementCache.click(function (e) {
// ...Behavior...
});
}
// There are four other links with a similar rebind/bind function relationship set up.
Naturally, I seized on the rebind being repeated so often with nearly the exact same code - ripe for a refactor. We have a common library namespace, where I added a rebindEvents function...
var MyCommon = function () {
var pub = {};
pub.rebindEvents = function($elementCache, selector, bindFunction) {
$elementCache = $(selector);
$elementCache.unbind();
bindFunction();
};
return pub;
}();
Upon trying to call that, and run the site, I immediately stubbed my toe on an UncaughtTypeError: method click cannot be called on object undefined.
As it turns out, it seems when I call the following:
MyCommon.rebindEvents($elementCache, '.some-class', bindSomeLink);
The $elementCache is not being passed to the rebindEvents method; when I step to it in my debugger, $elementCache inside of rebindEvents is undefined.
Some handy StackOverflow research revealed to me that JavaScript does not have referential-passing, at least in the C/C++/C# sense that I am familiar with, which leads me to my two Questions:
A) Is it even possible for me to refactor this rebind functionality with a cache reference pass of some sort?
B) If it's possible for me to refactor my rebind function to my common namespace, how would I go about doing it?
Use Jquery On to bind events at an element high-up in the DOM that is always present.
http://api.jquery.com/on/
This is the simplest way to handle event binding to dynamically created elements.
here is a jsfiddle that shows an example.
$('#temp').on('click', 'button', function(){
alert('clicked');
});
$('#temp').append('<button>OK</button>');
The event is bound to a div, which later has a button dynamically added. Because the button has no click event, the event "bubbles" up the DOM tree to its parent element which does have a click event for a button, so it handles it and the event "bubbles" no further.
So I used this totally awesome tool called Visual Event, which shows all the event handlers bound to an object - and I noticed that every time I clicked or played around with my object and checked the list of event handlers bound to it, there were and more every time. My problem is this one: console.trace or stack trace to pinpiont the source of a bug in javascript? After using Visual Event and someone else's suggestion, I'm thinking my problem is that I'm probably binding the same handlers to the same events over and over again. Is there a way to unbind things regularly?
My application has a bunch of plugins connect to dynamically created divs. These divs can be resized and moved around the place. The application is a kind of editor, so users arrange these divs (which contain either images or text) in any design they like. If the user clicks on a div, it becomes "activated", while all other divs on the page get "deactivated". I have a bunch of related plugins, like activateTextBox, initTextBox, deactivateTextBox, readyTextBox, and so on. Whenever a div is first created, the init plugin is called once, just the first time after creation, like so:
$(mydiv).initTextBox();
But readyTextBox and activateTextBox and deactivateTextBox are called often, depending on other user events.
In init, I first use bind things like resizable() and draggable(), then I make the box "ready" for use
$.fn.extend({
initTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.mouseenter(function(){
if(!$this.hasClass('activated'))
$this.readyTextBox();
}
$this.mouseleave(function(){
if($this.hasClass('ready')){
$this.deactivateTextBox();
$this.click(function(e){
e.preventDefault();
});
}
});
});
});
Here's a simplified summary version of the readyTextBox plugin:
(function($){
$.fn.extend({
readyTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.resizable({ handles: 'all', alsoResize: img_id});
$this.draggable('enable');
$this.on( "dragstop", function( event, ui )
{/* some function */ });
$this.on("resizestop", function( event, ui ){ /* another function */ });
// and so on
});
Then there's activateTextBox():
$.fn.extend({
activateTextBox: function(){
return this.each(function() {
// lots of code that's irrelevant to this question
$this.resizable('option','disabled',true); //switch of resize & drag
$this.draggable('option', 'disabled', true);
});
Then deactivate, where I turn on draggable and resizable again, using the code:
$this.draggable('enable'); $this.resizable('option','disabled',false);
These divs, or "textboxes" are contained within a bigger div called content, and this is the click code I have in content:
$content.click(function(e){
//some irrelevant code
if( /* condition to decide if a textbox is clicked */)
{ $(".textbox").each(function(){ //deactivate all except this
if($(this).attr('id') != $eparent.attr('id'))
$(this).deactivateTextBox();
});
// now activate this particular textbox
$eparent.activateTextBox();
}
});
This is pretty much the relevant code related to text boxes. Why is it that whenever I drag something around and then check Visual Event, there are more clicks and dragstops and mouseovers than before? Also, the more user interacts with the page, the longer the events take to complete. For example, I mouseout from a div, but the move cursor takes a loooong time to get back to default. I quit dragging, but everything gets stuck for a while before getting ready to take more user clicks, etc. So I'm guessing the problem has to be that I'm binding too many things to the same events need to be unbinding at some point? It gets so bad that draggable eventually stops working at some point. The textboxes just get stuck - they're still able to be resized, but dragging stops working.
Am I binding events over and over
Yes. Have a look at your code:
$this.mouseenter(function(){
…
$this.mouseleave(function(){
…
$this.click(function(e){
…
});
});
});
That means every time you mouseover the element, you add another leave handler. And when you leave the element, every of those handlers adds another click event.
I'm not sure what you want to do, but there are several options:
bind the event handlers only once, and keep track of the current state with boolean variables etc.
before binding, remove all other event handlers that are already bound. jQuery's event namespacing can help you to remove only those which your own plugin added.
use the one() method that automatically unbinds a listener after firing it.
Alright, so I'm making a Facebook-style chat. But that doesn't really matter.
See here:
http://jsfiddle.net/SkHme/7/
Nice and pretty, right? Well, there's a problem. Notice this line:
<div class="conversation EmperorCuzco" onclick="setActive('EmperorCuzco')">
See the onclick attribute? Well, it's not working. However, I have confirmed that the function itself DOES work. (if you run it just like that in the JavaScript, it runs like a dream) I have further confirmed that the function is not the problem by attempting to replace the onclick value with a simple alert('blah'), but that doesn't work either.
So, what's up? I'm guessing that something in my JavaScript is somehow disabling something, but I have absolutely no idea what it could be, nor how I could go about fixing it. I did some web searching, but couldn't find anything that helps.
What's going on?
Your setActive function is defined inside the scope of the $(document).ready handler. Move the function outside that function so that it is in the global scope.
Right now it looks like this:
$(document).ready(function()
{
// ...
function setActive(new_conversation)
{
// ...
}
});
Now change that to:
$(document).ready(function()
{
// ...
});
function setActive(new_conversation)
{
// ...
}
Really though, you should separate your content from your interactions and bind those event handlers in your script itself. Something like:
// Refers to the last clicked conversation *button*
// Initialize to empty jQuery object
var $active_conversation = $([]);
// Binding on #chat, targeting click events on .conversation_button children
$("#chat").on("click", ".conversation_button", function() {
// Hide currently active conversation
$active_conversation.hide();
$active_conversation.siblings('.conversation_button').removeClass("selected");
// Set as currently active conversation button
// Note: this refers to the clicked <div class="conversation_button">
var $this = $(this);
$active_conversation = $this.siblings('.conversation');
// Show this conversation
$active_conversation.show();
$this.removeClass("alert").addClass("selected");
});
Some advantages of this approach:
You don't need different classes for different conversations. By storing the actual conversation DOM element (as a jQuery object) in $active_conversation, the conversation can be identified without any extra processing.
You can add and remove whole list items with a conversation without assigning new event handlers. In the sample above, the event handler for all .conversation_button elements is defined at the level of #chat. For more about this binding mechanism, read up on .on (or .delegate for pre-1.7).
Here, have an updated fiddle! :-)
If all you say is really true (bad mistakes happen), the only thing that can make this is that an other event handler which takes your event before uses stopPropagation() or return false;
A quick check that can do is replace onclick with onmousedown or onmouseup, and see if you alert become visible.
What's the best way to execute a function exactly once every time a button is clicked, regardless of click speed and browser?
Simply binding a "click" handler works perfectly in all browsers except IE.
In IE, when the user clicks too fast, only "dblclick" fires, so the "click" handler is never executed. Other browsers trigger both events so it's not a problem for them.
The obvious solution/hack (to me at least) is to attach a dblclick handler in IE that triggers my click handler twice. Another idea is to track clicks myself with mousedown/mouseup, which seems pretty primitive and probably belongs in a framework rather than my application.
So, what's the best/usual/right way of handling this? (pure Javascript or jQuery preferred)
Depending on your situation you can use different approaches, but I would suggest using namespaced event handlers with jQuery like this:
function eventHandler(event) {
// your handler code here
doSomeMagic();
}
var element = $('#element');
element.one('click.someNameSpace', function(event){
// first we unbind all other event handlers with this namespace
element.unbind('.someNameSpace');
// then we execute our eventHandler
eventHandler();
}).one('dblclick.someNameSpace', function(event){
// If this fires first, we also unbind all event handlers
element.unbind('.someNameSpace');
// and then execute our eventHandler
eventHandler();
});
I'm not sure this will work the way you want it, but it's a start, I guess.
Mousedown and mouseup works just like the click functions, unfortunately so much that when IE omits a click because of a doubleclick it will also omit the mousedown and mouseup. In any case, you can add both click and dblclick to the same object and feed the clicks through a function that sort out any click happening too close to the last.
<div onclick="clk()" ondblclick="clk()"></div>
lastclicktime=0
function clk(){
var time=new Date().getTime()
if(time>lastclicktime+50){
lastclicktime=time
//Handle click
}
}
I by the way just found out that, at least in Firefox the dblclick event is not given an event time, therefore I had to resolve to the Date method.