Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've got this open source jQueryUI widget, which leaks memory whenever the DOM node is removed:

;(function($){
'use strict';

var pluginName  = 'vanderlee.tristate',
    tristates   = [],
    originalVal = $.fn.val,
    widget      = function(element) {
        return {
            element: $(element),

            _options: {
                state:              undefined,
                value:              undefined,  // one-way only!
                checked:            undefined,
                unchecked:          undefined,
                indeterminate:      undefined,

                change:             undefined,
                init:               undefined
            },

            _setOptions: function(options) {
                $.extend(this._options, options);
            },

            _create: function() {
                var that = this,
                    state;

                that.element.click(function(e) {
                    switch (that._options.state) {
                        case true:  that._options.state = null; break;
                        case false: that._options.state = true; break;
                        default:    that._options.state = false; break;
                    }
                    that._refresh(that._options.change);
                });

                that._options.checked       = that.element.attr('checkedvalue')       || that._options.checked;
                that._options.unchecked     = that.element.attr('uncheckedvalue')     || that._options.unchecked;
                that._options.indeterminate = that.element.attr('indeterminatevalue') || that._options.indeterminate;

                // Initially, set state based on option state or attributes
                if (typeof that._options.state === 'undefined') {
                    that._options.state     = typeof that.element.attr('indeterminate') !== 'undefined'? null : that.element.is(':checked');
                }
                // If value specified, overwrite with value
                if (typeof that._options.value !== 'undefined') {
                    state = that._parseValue(that._options.value);
                    if (typeof state !== 'undefined') {
                        that._options.state = state;
                    }
                }

                that._refresh(that._options.init);

                return this;
            },

            _refresh: function(callback) {
                var that    = this,
                    value   = this.value();

                that.element.data(pluginName, value);

                if (that._options.state === null) {
                    that.element.attr('indeterminate', 'indeterminate');
                    that.element.prop('indeterminate', true);
                } else {
                    that.element.removeAttr('indeterminate');
                    that.element.prop('indeterminate', false);
                }

                that.element.attr('checked', that._options.state);


                if ($.isFunction(callback)) {
                    callback.call(that.element, that._options.state, that.value());
                }
            },

            state: function(value) {
                if (typeof value === 'undefined') {
                    return this._options.state;
                } else if (value === true || value === false || value === null) {
                    this._options.state = value;

                    this._refresh(this._options.change);
                }
                return this;
            },

            _parseValue: function(value) {
                if (value === this._options.checked) {
                    return true;
                } else if (value === this._options.unchecked) {
                    return false;
                } else if (value === this._options.indeterminate) {
                    return null;
                }
            },

            value: function(value) {
                if (typeof value === 'undefined') {
                    var value;
                    switch (this._options.state) {
                        case true:
                            value = this._options.checked;
                            break;

                        case false:
                            value = this._options.unchecked;
                            break;

                        case null:
                            value = this._options.indeterminate;
                            break;
                    }
                    return typeof value === 'undefined'? this.element.attr('value') : value;
                } else {
                    var state = this._parseValue(value);
                    if (typeof state !== 'undefined') {
                        this._options.state = state;
                        this._refresh(this._options.change);
                    }
                }
            }
        };
    };

$.fn.tristate = function(operation, value) {
    if (typeof operation === 'undefined' || $.isPlainObject(operation)) {
        return this.each(function() {
            var that        = this,
                tristate,
                result;

            tristate = widget(this);
            tristate._setOptions(operation);
            tristate._create.apply(tristate);
            tristates.push(tristate);
        });
    } else {
        var that    = this,
            tristate,
            result;

        $.each(tristates, function() {
            if (this.element.is(that)) {
                result = this[operation].call(this, value);
                return false;
            }
        });

        return result;
    }
};

// Overwrite fn.val
$.fn.val = function(value) {
    var data = this.data(pluginName);
    if (typeof data === 'undefined') {
        if (typeof value === 'undefined') {
            return originalVal.call(this);
        } else {
            return originalVal.call(this, value);
        }
    } else {
        if (typeof value === 'undefined') {
            return data;
        } else {
            this.data(pluginName, value);
            return this;
        }
    }
};

// :indeterminate pseudo selector
$.expr.filters.indeterminate = function(element) {
    var $element = $(element);
    return typeof $element.data(pluginName) !== 'undefined' && $element.prop('indeterminate');
};

// :determinate pseudo selector
$.expr.filters.determinate = function(element) {
    return !($.expr.filters.indeterminate(element));
};

// :tristate selector
$.expr.filters.tristate = function(element) {
    return typeof $(element).data(pluginName) !== 'undefined';
};
}(jQuery));

The above code works fine and is used in production code. The only problem with it, is that it leaks memory when the DOM node it is attached to is removed. For instance on a single-AJAX-page site.

If I comment out both the element.click event binding in _create and the element.data() call in _refresh, the memory leak is gone (as well as functionality).

I've tried all kinds of solutions and fixes to known jQuery memory leak patterns, but nothing seems to work.

I've tried adding a destroy method and calling it before removing the DOM node, but that doesn't help either.

Does anybody how to prevent the memory leak?

Here's a JSFiddle to demonstrate the leakage: http://jsfiddle.net/mwvdlee/BdCnL/

share|improve this question
    
Unfortunately this site is for reviewing 'working' code. Because you're explicitly asking about how to fix a 'bug' (i.e. a memory leak), people think this question is off-topic. You might be able to get an answer on StackOverflow. –  ChrisW Feb 6 at 13:14
1  
does this work? could you rephrase the question a little bit, I am going to vote to reopen. it sounds like the code works but is not optimal. in which case it is on-topic here, in my opinion. –  Malachi Feb 6 at 14:20
    
I've added a bit that the code is indeed working. The memory leak only happens when removing DOM nodes, which is only a problem on certain types of situations. –  Martijn Feb 6 at 21:23
    
@Martijn Have you tried profiling? –  Jivings Feb 6 at 21:32
    
Js Memory leak is not a bug. Its a feature. Keeps out people with old browsers. –  James Khoury Feb 7 at 3:39
show 4 more comments

closed as off-topic by David Harkness, tinstaafl, Nikita Brizhak, MrSmith42, Jamal Feb 7 at 9:56

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Your question must contain working code for us to review it here. For questions regarding specific problems encountered while coding, try Stack Overflow. After getting your code to work, you may edit this question seeking a review of your working code." – David Harkness, tinstaafl, Nikita Brizhak, MrSmith42, Jamal
If this question can be reworded to fit the rules in the help center, please edit the question.

Browse other questions tagged or ask your own question.