具有Backbone内存泄漏的Bootstrap Typeahead

时间:2013-06-07 14:06:02

标签: backbone.js memory-leaks garbage-collection bootstrap-typeahead

我正在使用Bootstrap Typeahead和Backbone来显示基于用户输入从服务器获取的数据。 Backbone集合用于存储获取的数据,在应用启动时创建一次。该集合将重复用于Typeahead中的每个新搜索。

我遇到的问题是每次用户搜索某些内容时,浏览器内存使用率都会持续上升。我的问题是,如何确保新的结果/数据在新的收集时被垃圾收集?是否正在重新使用相同的集合进行新的搜索?

集合js文件

define([
"data",
"backbone",
"vent"
],
function (data, Backbone, vent) {
    var SearchCollection = Backbone.Collection.extend({

        model:Backbone.Model,

        url:function () {
            return data.getUrl("entitysearch");
        },

        initialize:function (models, options) {
            var self=this;
            this.requestAborted = false;
            this.categories = (options && options.categories) || ['counterparty', 'company', 'user'];
            this.onItemSelected = options.onItemSelected;
            this.selectedId = options.selectedId; // should be prefixed with type eg. "company-12345"
            _.bindAll(this, "entitySelected");

            vent.bindTo(vent, "abortSearchAjax", function () {
                this.requestAborted = true;
            }, this);
        },

        search:function (criteria) {
            var self = this,
                results = [];

            // abort any existing requests
            if (this.searchRequest) {
                this.searchRequest.abort();
            }
            self.requestAborted= false;
            this.searchRequest = this.fetch({
                data:$.param({
                    query:criteria,
                    types: this.mapEntityTypesToCodes(this.categories),
                    fields:'ANY',
                    max: 500
                })
            })

            .done(function(response, textStatus, jqXHR) {
                    if (!self.requestAborted){
                        results = self.processResponse(response);
                    }

            })
            .fail(function(jqXHR, textStatus, errorThrown) {
                 if(errorThrown === "Unauthorized" || errorThrown === "Forbidden") {
                    alert("Either you do not have the right permissions to search for entities or you do not have a valid SSO token." +
                        " Reload the page to update your SSO token.");
                 }
            })
            .always(function(){
                if (!self.requestAborted){
                    self.reset(results);
                    self.trigger('searchComplete');
                }
                });
        },
        /**
         * Backbone parse won't work here as it requires you to modify the original response object not create a new one.
         * @param data
         * @return {Array}
         */
        processResponse:function (response) {
            var self = this,
                result = [];

            _.each(response, function (val, key, list) {
                if (key !== 'query') {
                    _.map(val, function (v, k, l) {
                        var id;
                        v.type = self.mapEntityShortName(key);
                        id = v.id;
                        v.id = v.type + '-' + v.id;
                        v.displayId = id;
                    });
                    result = result.concat(val);
                }
            });
            return result;
        },

        mapEntityTypesToCodes:function (types) {
            var codes = [],
                found = false;
            _.each(types, function(el, index, list) {
                {
                    switch (el) {
                        case 'counterparty':
                            codes.push('L5');
                            found = true;
                            break;
                        case 'company':
                            codes.push('L3');
                            found = true;
                            break;
                        case 'user':
                            codes.push('user');
                            found = true;
                            break;
                    }
                }
            });
            if (!found) {
                throw "mapEntityTypesToCodes - requires an array containing one or more types - counterparty, company, user";
            }
            return codes.join(',');
        },

        mapEntityShortName: function(name) {
            switch (name) {
                case 'parties':
                    return 'counterparty';
                    break;
                case 'companies':
                    return 'company';
                    break;
                case 'users':
                    return 'user';
                    break;
            }
        },

        entitySelected:function (item) {
            var model,
                obj = JSON.parse(item),
                data;

            model = this.get(obj.id);
            if (model) {
                model.set('selected', true);
                data = model.toJSON();
                this.selectedId = obj.id;
                //correct the id to remove the type eg. company-
                data.id = data.displayId;
                this.onItemSelected && this.onItemSelected(data);
            } else {
                throw "entitySelected - model not found";
            }
        },

        openSelectedEntity: function() {
            var model = this.get(this.selectedId);
            if (model) {
                vent.trigger('entityOpened', {
                    id: this.selectedId.split('-')[1],
                    name: model.get('name'),
                    type: model.get('type')
                });
            }
        },

        entityClosed:function (id) {
            var model;

            model = this.where({
                id:id
            });
            if (model.length) {
                model[0].set('selected', false);
            }
        }

    });

    return SearchCollection;
});

查看js文件

define([
"backbone",
"hbs!modules/search/templates/search",
"bootstrap-typeahead"
],

function (Backbone, tpl) {
    return Backbone.Marionette.ItemView.extend({

        events: {
            'click .action-open-entity': 'openEntity'
        },

        className: 'modSearch',

        template:{
            type:'handlebars',
            template:tpl
        },

        initialize:function (options) {
            _.bindAll(this, "render", "sorter", "renderSearchResults", "typeaheadSource");
            this.listen();
            this.categoryNames = options.categoryNames;
            this.showCategoryNames = options.showCategoryNames;
            this.autofocus = options.autofocus;
            this.initValue = options.initValue;
            this.disabled = options.disabled;
            this.updateValueOnSelect = options.updateValueOnSelect;
            this.showLink = options.showLink;
            this.resultsLength = 1500;
        },

        listen:function () {
            this.collection.on('searchComplete', this.renderSearchResults);
            this.collection.on('change:selected', this.highlightedItemChange, this);
        },

        resultsFormatter:function () {
            var searchResults = [],
                that = this;

            this.collection.each(function (result) {
                searchResults.push(that._resultFormatter(result));
            });
            return searchResults;
        },

        _resultFormatter:function (model) {
            var result = {
                name:model.get('name'),
                id:model.get('id'),
                displayId: model.get('displayId'),
                aliases:model.get('aliases'),
                type:model.get('type'),
                marketsPriority:model.get('marketsPriority')
            };
            if (model.get('ssoId')) {
                result.ssoId = model.get('ssoId');
            }
            return JSON.stringify(result);
        },

        openEntity: function() {
            this.collection.openSelectedEntity();
        },

        serializeData:function () {
            return {
                categoryNames:this.categoryNames,
                showCategoryNames: this.showCategoryNames,
                initValue:this.initValue,
                autofocus:this.autofocus,
                showLink:this.showLink
            };
        },

        onRender:function () {
            var self = this,
                debouncedSearch;

            if (this.disabled === true) {
                this.$('input').attr('disabled', 'disabled');
            } else {
                debouncedSearch = _.debounce(this.typeaheadSource, 500);
                this.typeahead = this.$('.typeahead')
                    .typeahead({
                        source: debouncedSearch,
                        categories:{
                            'counterparty':'Counterparties',
                            'company':'Companies',
                            'user':'Users'
                        },
                        minLength:3,
                        multiSelect:true,
                        items:this.resultsLength,
                        onItemSelected:self.collection.entitySelected,
                        renderItem:this.renderDropdownItem,
                        matcher: this.matcher,
                        sorter:this.sorter,
                        updateValueOnSelect:this.updateValueOnSelect
                    })
                    .data('typeahead');
                $('.details').hide();
            }

        },

        onClose: function(){
            this.typeahead.$menu.remove();
        },

        highlightedItemChange:function (model) {
            this.typeahead.changeItemHighlight(model.get('displayId'), model.get('selected'));
        },

        renderSearchResults:function () {
            this.searchCallback(this.resultsFormatter());
        },

        typeaheadSource:function (query, searchCallback) {
            this.searchCallback = searchCallback;
            this.collection.search(query);
        },

        /**
         * Called from typeahead plugin
         * @param item
         * @return {String}
         */
        renderDropdownItem:function (item) {
            var entity,
                marketsPriority = '',
                aliases = '';
            if (!item) {
                return item;
            }
            if (typeof item === 'string') {
                entity = JSON.parse(item);
                if (entity.marketsPriority && (entity.marketsPriority === "Y")) {
                    marketsPriority = '<span class="marketsPriority">M</span>';
                }
                if (entity.aliases && (entity.aliases.constructor === Array) && entity.aliases.length) {
                    aliases = ' (' + entity.aliases.join(', ') +  ') ';
                }
                if (entity.type === "user"){
                    entity.displayId = entity.ssoId;
                }
                return [entity.name || '', aliases, ' (', entity.displayId || '', ')', marketsPriority].join('');
            }
            return item;
        },

        matcher: function(item){
            return item;
        },

        /**
         * Sort typeahead results - called from typeahead plugin
         * @param items
         * @param query
         * @return {Array}
         */
        sorter:function (items, query) {
            var results = {},
                reducedResults,
                unmatched,
                filteredItems,
                types = ['counterparty', 'company', 'user'],
                props = ['displayId', 'name', 'aliases', 'ssoId'],
                type,
                prop;

            query = $.trim(query);
            for (var i = 0, j = types.length; i < j; i++) {
                type = types[i];
                filteredItems = this._filterByType(items, type);
                for (var k = 0, l = props.length; k < l; k++) {
                    prop = props[k];
                    unmatched = [];
                    if (!results[type]) {
                        results[type] = [];
                    }
                    results[type] = results[type].concat(this._filterByProperty(query, filteredItems, prop, unmatched));
                    filteredItems = unmatched;
                }
            }

            reducedResults = this._reduceItems(results, types, this.resultsLength);

            return reducedResults;
        },

        /**
         * Sort helper - match query string against a specific property
         * @param query
         * @param item
         * @param fieldToMatch
         * @param resultArrays
         * @return {Boolean}
         * @private
         */
        _matchProperty:function (query, item, fieldToMatch, resultArrays) {

            if (fieldToMatch.toLowerCase().indexOf(query.toLowerCase()) === 0) {
                resultArrays.beginsWith.push(item);
            } else if (~fieldToMatch.indexOf(query)) resultArrays.caseSensitive.push(item)
            else if (~fieldToMatch.toLowerCase().indexOf(query.toLowerCase())) resultArrays.caseInsensitive.push(item)
            else if(this._fieldConatins(query, fieldToMatch, resultArrays)) resultArrays.caseInsensitive.push(item)
            else return false;
            return true;
        },

        _fieldConatins:function (query, fieldToMatch, resultArrays) {
            var matched = false;
            var queryList = query.split(" ");
            _.each(queryList, function(queryItem) {
                if(fieldToMatch.toLowerCase().indexOf(queryItem.toLowerCase()) !== -1) {
                    matched = true;
                    return;
                }
            });
            return matched;
        },

        /**
         * Sort helper - filter result set by property type (name, id)
         * @param query
         * @param items
         * @param prop
         * @param unmatchedArray
         * @return {Array}
         * @private
         */
        _filterByProperty:function (query, items, prop, unmatchedArray) {
            var resultArrays = {
                    beginsWith:[],
                    caseSensitive:[],
                    caseInsensitive:[],
                    contains:[]
                },
                itemObj,
                item,
                isMatched;

            while (item = items.shift()) {
                itemObj = JSON.parse(item);
                isMatched = itemObj[prop] && this._matchProperty(query, item, itemObj[prop].toString(), resultArrays);
                if (!isMatched && unmatchedArray) {
                    unmatchedArray.push(item);
                }
            }
            return resultArrays.beginsWith.concat(resultArrays.caseSensitive, resultArrays.caseInsensitive, resultArrays.contains);
        },

        /**
         * Sort helper - filter result set by entity type (counterparty, company, user)
         * @param {Array} items
         * @param {string} type
         * @return {Array}
         * @private
         */
        _filterByType:function (items, type) {
            var item,
                itemObj,
                filtered = [];

            for (var i = 0, j = items.length; i < j; i++) {
                item = items[i];
                itemObj = JSON.parse(item);
                if (itemObj.type === type) {
                    filtered.push(item);
                }
            }
            return filtered;
        },

        /**
         * Sort helper - reduce the result set down and split between the entity types (counterparty, company, user)
         * @param results
         * @param types
         * @param targetLength
         * @return {Array}
         * @private
         */
        _reduceItems:function (results, types, targetLength) {
            var categoryLength,
                type,
                len,
                diff,
                reduced = [],
                reducedEscaped = [];

            categoryLength = Math.floor(targetLength / types.length);

            for (var i = 0, j = types.length; i < j; i++) {
                type = types[i];
                len = results[type].length;
                diff = categoryLength - len;
                if (diff >= 0) { // actual length was shorter
                    reduced = reduced.concat(results[type].slice(0, len));
                    categoryLength = categoryLength + Math.floor(diff / (types.length - (i + 1)));
                } else {
                    reduced = reduced.concat(results[type].slice(0, categoryLength));
                }
            }
            _.each(reduced, function(item) {
                item = item.replace(/\'/g,"`");
                reducedEscaped.push(item);
            });
            return reducedEscaped;
        }

    });
});

0 个答案:

没有答案