views:

210

answers:

6

I have a predicament: I want to send some data with an event listener but also be able to remove the listener. Here's the standard closure approach...

var fn = function(e){method(e,data)};
el.addEventListener('click',fn,false);
el.removeEventListener('click',fn,false);

and you could remove the event, just fine. But say, the element was removed from the DOM? Then, you'd be left with the fn function sitting around. After removing a couple thousand DOM elements, it will result in something of a memory leak.

I've considered attaching a DOMNodeRemoved event handler, that would remove any left over functions/data along with the removed node. But apparently, that event isn't cross-browser compatible.

The only other option I've come up with would be modifying the element's DOM. Consider...

el.MyEventData = function(e){method(e,data)};
el.addEventListener('click',el.MyEventData,false);
el.removeEventListener('click',el.MyEventData,false);

Is modifying the DOM acceptable in this situation? The only sticky part of that solution is when you try to add more than one event listener. Let's say we made a custom function to parse the adding/removing of events...

function makeEvent(fn,data){
    var dataFn = function(e){fn(e,data)};
    //create object to hold all added events
    el.myEvents = {};
    //make ID for this specific event
    var eventID = ranString();
    //add the event to the events object
    el.myEvents[eventID] = [fn,dataFn];
    //finally add the listener
    el.addEventListener('click',dataFn,false);
}
function destroyEvent(fn){
    //find all fn references
    for(var id in el.myEvents){
        if (el.myEvents[id][0] == fn){
            el.removeEventListener('click',el.myEvents[id][1],false);
            el.myEvents[id] = null;
        }
    }
}

It still modifies the DOM, as before, and certainly isn't a very elegant solution either. Does anyone know of any alternative, better method for passing data?

EDIT: So, I've looked into a little of jQuery's data/event scripts. I don't completely understand the code, so if someone would clarify, it would be helpful. But it seems as though they use a similar method, by making some type of el.cache property, that holds event data.

A: 

A possible solution to maybe take you in a different direction: add the function as an inline sibling of the element.

<span id='element12345'>Test</span><script 
  type='text/javascript'>function fn12345() { /* ... */ }</script>

Then, when you remove all the event listeners that you want, you can also remove the "nextSibling()" of the element you're working with.

Steve
A: 

how about a setup like this? (using IE syntax since that's what I have available right now)

<div id="clickbox" style="width: 100px; height: 100px; border: 1px solid orange;">
    click here to test</div>
<input id="Button1" type="button" value="clear handler" />
<script>

    var data = "derp1";
    var el = document.getElementById('clickbox');
    var btn = document.getElementById('Button1');

    // methods
    var method = function (e, dat2) { alert(dat2); };
    var fn = function (e) { method(e, data) };
    var remover = null;

    // attachment
    el.attachEvent('onclick', fn, false);

    (function (id, handler) {

        // handler variable is local but points to the right function
        remover = function (e) {

            if (document.getElementById(id)) {
                // remove the original listener (handler is fn)
                document.getElementById(id).detachEvent('onclick', handler, false);
                alert('detached');
            }

            // remove last reference to the original method
            handler = null;
            alert('method nulled');

            // clean up the remover method
            e.srcElement.detachEvent('onclick', remover);
            remover = null;
        };

        btn.attachEvent('onclick', remover);

    })('clickbox', fn);

    // clear the original variable but the method still exists as an event listener
    fn = null;

    // you should be able to remove the div element and any references to it 
    // without leaving any stray bits around.

    setTimeout( function() {
                     var d = document.getElementById('clickbox');
                     if (d){ d.parentNode.removeChild(d) ; }
     } , 6000 );

    el = null;
    btn = null;

</script>

I'm assuming you don't want the listener removed immediately after adding it but rather want to be able to remove it at a later time. to deal with this, the cleanup routine is given its own scope by creating an anonymous function which is immediately invoked with fn as a parameter. the anon function then has its own reference to fn maintained in the handler variable. after that, fn can be cleaned up and the remaining references to the original method exist in the listener list for your element and in the scope of the anonymous function.

within the anonymous function scope, the function remover has access to the handler variable and can use it to detach the listener. remover then detaches and clears itself so there should be nothing left with access to any version of fn/handler.

I don't have any way to verify all this right now but I think it makes sense and should hold up in modern browsers.

lincolnk
So if I get what you're trying to do, the script allows removing the attached event by registering a remove event with another element. That way, when I clicked on the remove button, it would already have a reference to the function, and remove it. But is there any way the script allows to remove the event by simply calling some JavaScript code-- without adding another event to another element?
Azmisov
the button click is just a way to demo the script by calling `remover`. if you eliminate `btn.attachEvent/detachEvent` calls from this script, `remover` will hang around as a pre-defined cleanup function until you're ready to use it. also you could replace the `e` parameter with whatever parameters you need, or nothing. it's only there to clean up the extra demo stuff.
lincolnk
You would still have to have a separate `remover` for each event you register, since each must have a unique reference to each event's handler function (since the handlers may be calling different methods). I've checked your demo, and after the element is removed, the remover function still exists. I need some way of destroying the remover (or cache), so there aren't any memory leaks.
Azmisov
@Azmisov `remover` exists until you call it and it cleans itself up. alternatively, if you don't set it as a handler and so `remover` is the only reference to that function, you could null it whenever you cut the original element. but yest you would need multiple removers for multiple elements.
lincolnk
If that's the case, I'd either have to either store a reference to the registered function (as I do now), or a reference to the remover function (store them so I can call each element's specific remover function). Either way, it comes down to the same problem: When an element is removed from the page (without any other script knowing about it), the function references will start to pile up, causing a memory leak.
Azmisov
@Azmisov if you want to be able to leave an element on the page but detach its listener (which is part of the original question as I understood id) you're going to have to hold a reference to the listener somewhere. holding a remover gives you the benefit of having a cleanup method, and if you're removing elements outright it should be pretty easy to call the remover at the same time (have a list of id:remover associations or something).
lincolnk
+2  A: 

Considering that you use addEventListener this is not an issue as all modern garbage collectors can take care of such situations. The problem with event listeners only exists in IE's implementation (7-).

Test - 10 000 addEventListener and remove element (see Windows Task Manager)

When a DOM object contains a reference to a JavaScript object (such an event handling function), and when that JavaScript object contains a reference to that DOM object, then a cyclic structure is formed. This is not in itself a problem. At such time as there are no other references to the DOM object and the event handler, then the garbage collector (an automatic memory resource manager) will reclaim them both, allowing their space to be reallocated. The JavaScript garbage collector understands about cycles and is not confused by them.

http://www.crockford.com/javascript/memory/leak.html

galambalazs
you don't address removing a listener from an element that he wants to keep around.
lincolnk
because if you add a listener and then you remove it nothing bad happens at all. Even in IE. Problems (leak) only occur when you create cyclic references and then you remove elements without clearing event handlers beforehand.
galambalazs
A: 

Did you consider .delegate()?

Rimantas
Very interesting. Not a standard function, I believe, but I will look into it.
Azmisov
I believe .apply() or .call() are the actual functions needed. However, they still don't solve the problem.
Azmisov
A: 

According to your jQuery question:

Each jQ object has a data property. It does not stored inside the element itself - it's very important. jQ use general storage for all elements - jQuery.cache. So when you add anything to the element like this:

$('#myEl').data('someValue', 1);

jQ do the following:

jQuery.cache[elementUniqId]['someValue'] = 1;

So element does not contain its data object. It only have an uniq id that is allows it to access to the data recorde at the global storage. (elementUniqId is autogenerated)

jQ events are stored into the element data as well:

$('#myEl').click(function() {  first listener });
$('#myEl').mouseenter(function() {  one more listener });
$('#myEl').click(function() {  anotheer listener });

Will be stored:

jQuery.cache[elementUniqId]['events'] = {
    click: [function() { first listener }, function() { anotheer listene }],
    mouseenter: [function() { one more listener }]
};

It allows jQ to store the order of execution for all listeners attached to each event. And later, when you delete dom element, using jQuery - .remove(), jQuery loops through the jQuery.cache[elementUniqId]['events'] and remove each listener from the element, and after removes element cache record. It allows jQ to preven memory leaks

fantactuka
So, jQuery can only removes the cache if .remove() was used? Say, if an ajax request changed the page, jQuery wouldn't remove the cache of the previous page?
Azmisov
Correct, so I assume that in case you're reloading content lots of times with adding events, etc. you should first do 'remove' on it before inserting new one. On the other hand, since you're adding event not dirrectly to the element, but into its record in general storage, it shouldn' cause lots of leaks. Or if possible you can use event delegation to the loaded content.
fantactuka
A: 

why not take a look at this

Binding Events To Non-DOM Objects With jQuery

http://www.bennadel.com/blog/1520-Binding-Events-To-Non-DOM-Objects-With-jQuery.htm

undertakeror
Interesting, but nothing helpful on sending data.
Azmisov