Ajax Client Control - memory leak - javascript

I am creating an Ajax Client Control in ASP.Net. By inheriting from IScriptControl and then adding the relavant javascript class (which would inherit from a javascript control). I have found a memory leak in the following code:
Type.registerNamespace("mynamespace");
myClass = function (element) {
myClass.initializeBase(this, [element]);
}
myClass.prototype = {
initialize: function () {
myClass.callBaseMethod(this, 'initialize');
var me = this;
$(document).ready(function () {
me._initializeControl();
me._hookupEvents();
});
},
dispose: function () {
//Add custom dispose actions here
myClass.callBaseMethod(this, 'dispose');
},
//...other code ...
_hookupEvents: function () {
var me = this;
var e = this.get_element();
$("#viewRates", e).click(function () {
me.openDialog();
});
},
//...other code...
myClass.registerClass('myClass', Sys.UI.Control);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
_hoookupEvents is a function in my javascript file. The leak is related ot the line me.openDialog. If I remove this line, there is no leak. However, I need this line to be able to call a function from the class (I cannot just use 'this' in the function because it would refer to the button). Is there a better way to do this? Or maybe I just need to call some methods in the dispose function to clean such things?

The memory leak at this code can happen on this line, as you also note
$("#viewRates", e).click(function () {
me.openDialog();
});
when you call it with UpdatePanel, or in general call it for the same component and with out first clear the previous events for the click, the previous handler stay on, and here we have two cases.
To register the same click event more than ones.
To update the dom with ajax, and not previous clear that handlers, as results the previous code stay for ever (for ever == until you leave the page).
In general the solution is to clear any previous handler for the click,
before add a new one.
when initialize a new ajax call with UpdatePanel and before get the new response.
Use a function like that to remove the click and clear the resource for the handler.
this.get_events().removeHandler('click');

I'm extremely hesitant to call it a memory leak if there are only 2 instance of myclass. If there are 2,000 instances of myclass there's DEFINITELY a leak.
I'd search real hard for any dynamic instantiation statements that you have, that create myClass on certain conditions. That is what i see a lot (creating classes in loops at application init, perhaps a form submit can trigger instantiation and it wasn't fully QA'd to see if you can get a submission to create multiple objects, etc).

Related

How do I assign a pre and post function on button click without changing button definition or previously written functions

I have the following buttons:
<button id="abcd" onclick="something()">click</button>
and the following functions are attached to this button apart from the one in its html definition.
$('#abcd').on('click',function(){alert("abcd");});
$('#abcd').on('click',function(){
someAjaxCallWithCallback;
});
Now I want a new function with another ajax call to execute on this button's click, before the above mentioned functions. This new function determines whether the remaining functions would be called or not based on what data is recieved by the ajax call. That is, this pre function should complete its execution before giving control over to the rest of the functions and also determine whether they would run or not.
As an example, without changing the existing validation logics and button code, I have to add a new pre-validation function and similarly and post validation function.
I have a bindFirst method using which I can at least bring my new function to the beginning of the call stack but I have not been able to contain its execution and control further delegation because of callbacks.
If I understand correctly, you are looking for the way to do this, without modifying html and already existing js, only by adding new js-code.
First of all, if onclick handler is set and you want to control it, you should disable it on page load (maybe, saving it to some variable):
$(document).ready(function() {
var onclick = $("#abcd").attr("onclick").split("(")[0];
//to run it in future: window[onclick]();
$("#abcd").attr("onclick", "");
});
Edit: I changed my answer a little, previous approach didn't work.
Now you need to remove all already existing handlers. If number of handlers you want to control is limited, constant and known to you, you can simply call them in if-else after pre-validation inside your pre-function. If you want something more flexible, you are able to get all the handlers before removing, save them and then call them in a loop.
For that "flexible" solution in the end of $(document).ready(); you save all already existing handlers to an array and disable them. Then you write your pre-function and leave it as the only handler.
var handlers = ($._data($("#abcd")[0], "events")["click"]).slice();
$("#abcd").off("click");
$("#abcd").click(function() {
//this is your pre-func
//some code
handlers[1].handler.call();
});
Try console.log($._data($("#abcd")[0], "events")) to see, what it is.
Finally just run your post-function and do whatever you need, using conditions.
So, the general algorithm is as follows:
Disable onclick
Save all handlers
Disable all handlers
Run pre-func first
Run handlers you want to be executed
Run post-func
In fact, you just make your pre-func the only handler, which can run all other handlers you may need.
Although Alex was spot on, I just wanted to add more details to cover certain cases that were left open.
class preClass{
constructor(name,id){
if($(id) && $(id)[0] && $(id)[0]['on'+name])
{
var existing = $(id)[0]['on'+name]
$(id).bindFirst(name,existing);
$(id).removeAttr('on'+name)
alert("here");
}
if($._data($(id)[0],"events")){
this.handlers = $._data($(id)[0],"events")[name].slice();
}
else
{
this.handlers = null;
}
this.id = id;
this.name = name;
}
generatePreMethod(fn,data)
{
$(this.id).off(this.name);
$(this.id).bindFirst(this.name,function(){
$.when(fn()).then(execAll(data));
});
}
}
function exec(item,index){
item.handler.call()
}
function execAll(handlers){
return function(){ handlers.forEach(exec);}
}
This more or less takes care of all the cases.
Please let me know if there is something I missed!

Calling a service using EventListeners

Disclaimer: Title is not super exact.
I have a service with a public method openCamera that calls a library in the global object and attaches eventlisteners to its HTML elements. When I call this service method from the HTML elements events, it works fine, but not when I call it via events attached to the window element. An example below:
class ImageService {
public static AttachEventOnce = false;
public openCamera() {
let camera = new JpegCamera('myContainer');
// captureBtn and videoBox are HTML elements generated from the library once instantiated
camera.captureBtn.addEventListener('click', () => this.openCamera()); // this works fine
camera.videoBox.addEventListener('resize', () => this.openCamera()); // doesn't enter here ie not calling it
if (!ImageService.AttachEventOnce) {
var that = this;
window.addEventListener('resize', () => that.openCamera()); // the buttons stop working
ImageService.AttachEventOnce = true;
}
};
}
The logic have been somewhat minified but more or less the same. I just want to call the service method again and again when window is resized. I don't care where I attach the listener (HTML element generated from the library or window).
My take: The window seem to retain the older object reference too as well as other listeners for other buttons which I think is causing the issue.
The window seem to retain the older object reference too as well as other listerers for other buttons which I think is causing the issue.
Correct — not because it's on window, but because you're using an arrow function and only hooking the event once, on the first instance where openCamera is called. So it doesn't matter whether that instance is discarded by everything else, it's the only instance that will receive that resize event. (There's also no reason for the var that = this; thing and then using that inside the function; arrow functions close over this like they do variables, so it does exactly what just using this within the function would do.)
It's not clear why you're doing that as opposed to hooking the event in an instance-specific way like you are the other events. If you remove the logic hooking it only once, you'll get the per-instance behavior.
Separately: It's odd to be attaching new event handlers every time the event occurs. You'll very quickly have them stacking up. The first time (say) you receive a click, you'll add a second click handler; the next time you receive a click, you'll receive two of them (one for each handler), and add two more handlers; and so on, doubling every time. This is bad enough with clicks, but disasterous with resize as there are a lot of resize events triggered when the window is being resized.

how can I create an event handler in a loop that does not recreate itself?

I have a class member lets call it remove. so to call it I write this.remove(arg)
I am building a table of buttons that I need this function to be an event of.
so the current code is
var me = this
for (x iterations) {
button.addEventListener('click',function() {
me.remove(this) // <- document is passed via this. I need both contexts
}
obviously this is bad code on every reiteration the function is being recreated.
remove.call()
wouldn't work because i don't have access to the new this context until it's created.
is they're a better way to write this as to not recreate the function every time?
It's not very clear what you're asking, but you can avoid recreating the function in the loop:
var me = this;
function removeButton() {
me.remove(this);
}
for (/*x iterations*/) {
button.addEventListener('click', removeButton);
}

Replacing all element's click bindings with a single document.on('click')?

I've always added click listeners to every separate element that needs to be listened, which can create a big messy Javascript with a lot of event bindings.
I was now thinking of doing it another way; by binding the click event to the entire document and upon click, see if the targeted element has a 'data-action' attribute and if present, execute the function in it. So that clicking:
Will execute function ajax_load_stuff()
It would make my code much cleaner, especially in ajax environments, but I want to know about performance and efficiency of this method. Are there any disadvantages to this approach?
UPDATE code example:
document.body.addEventListener("click", function (e) {
if (e.target) {
var action = e.target.getAttribute("data-action");
if (action) {
e.stopPropagation();
var params = e.target.getAttribute("data-params");
var data = [];
if (params) {
data = params.split(',');
}
window[action].apply(e.target, data);
}
}
}, false);
Ofcourse this approch has several advantages and disadvantages.
First discussing the disadvantages.
Need to handle event propagation perfectly otherwise it could make your system slow.
Passing parameter to click event will be difficult. Maybe need to introduce another attribute like : data-action-param
Advantages:
Less event handling code.

Click binding in jQuery with backbone

I'm using Backbone.js, and in one of my main views I've encountered a very strange bug that I can't for the life of me figure out how to solve.
The view looks a look like the new Twitter layout. It receives an array of objects, each of which describes a collection and views elements that act on that collection. Each collection is represented by one tab in the view. The render() method on my view takes this array of collection objects, clears out the tabContainer DOM element if it isn't already empty, renders the tabs and then binds events to each of those tabs.
Now in my code I have the method to render the tabs and the method to bind the click handlers to those tabs sequentially. This works fine the first time I execute render(), but on subsequent calls of render(), the click handlers are not bound. Here's the relevant code snippet:
initialize: function() {
// Context on render(), _addAllTabs and _bindTabEvents is set correctly to 'this'
_.bindAll(this, 'render', 'openModel', 'closeModel', 'isOpen', 'addAllModels', '_switchTab',
'addOneModel', '_addTab', '_removeTab', '_addAllTabs', '_loadCollection',
'_renderControls', '_setCurrentCollection', '_loadModels', '_bindTabEvents');
this.template = JST['ui/viewer'];
$(this.el).html(this.template({}));
// The tabContainer is cached and always available
this.tabContainer = this.$("ul.tabs");
this.collectionContainer = this.$("#collection_container");
this.controlsContainer = this.$("#controls");
this.showMoreButton = this.$("#show_more_button");
},
render: function(collections, dashboard) {
// If _bindTabEvents has been called before, then this.tab exists. I
// intentionally destroy this.tabs and all previously bound click handlers.
if (this.tabs) this.tabContainer.html("");
if (collections) this.collections = collections;
if (dashboard) this.$("#dashboard").html(dashboard.render().el);
// _addAllTabs redraws each of the tabs in my view from scratch using _addTab
this._addAllTabs();
// All tabs *are* present in the DOM before my _bindTabEvents function is called
// However these events are only bound on the first render and not subsequent renders
this._bindTabEvents();
var first_tab = this.collections[0].id;
this.openTab(first_tab);
return this;
},
openTab: function (collectionId, e) {
// If I move _bindTabEvents to here, (per my more thorough explanation below)
// my bug is somehow magically fixed. This makes no friggin sense.
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
_addAllTabs: function() {
_.each(this.collections, this._addTab );
},
_bindTabEvents: function() {
this.tabs = _.reduce(_.pluck(this.collections, "id"), _.bind(function (tabEvents, collectionId) {
var tabId = this.$("#" + collectionId + "_tab");
tabEvents[collectionId] = tabId.click(_.bind(this._switchTab, this, collectionId));
return tabEvents
}, this), {});
},
_addTab: function(content) {
this.tabContainer.append(
$('<li/>')
.attr("id", content.id + "_tab")
.addClass("tab")
.append($('<span/>')
.addClass('label')
.text(content.name)));
//this._loadCollection(content.id);
this.bind("tab:" + content.id, this._loadCollection);
pg.account.bind("change:location", this._loadCollection); // TODO: Should this be here?
},
etc..
As I said, the render() method here does work, but only the first time around. The strange part is that if I move the line this._bindTabEvents(); and make it the first line of the openTab() method like in the following snippet, then the whole thing works perfectly:
openTab: function (collectionId, e) {
this._bindTabEvents();
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
Of course, that line of code has no business being in that method, but it does make the whole thing work fine, which leads me to ask why it works there, but doesn't work sequentially like so:
this._addAllTabs();
this._bindTabEvents();
This makes no sense to me since, it also doesn't work if I put it after this line:
var first_tab = this.collections[0].id;
even though that is essentially the same as what does work insofar as execution order is concerned.
Does anyone have any idea what I'm doing wrong and what I should be doing to make this correct (in terms of both behavior and coding style)?
In your view's render function, return this.delegateEvents(); I think you are losing your event bindings across your renderings and you need to re-establish them.
See this link for the backbone.js documentation for that function:
backbone.js - delegateEvents
When you switch tabs you are not simply showing/hiding content you are destroying and rebuild dom element so you are also destroying event liseners attached to them. that is why the events only work once and why adding _bindTabEvents into render works, because you are re-attaching the events each time.
when this line executes : this.tabContainer.html(""); poof... no more tabs and no more tab events.

Categories