в jQuery пользовательского интерфейса автозаполнение комбобокса очень медленно с большими списках выбора


Я использую модифицированную версию jQuery UI Autocomplete Combobox, как видно здесь: http://jqueryui.com/demos/autocomplete/#combobox

ради этого вопроса, допустим, у меня есть именно этот код ^^^

при открытии combobox, либо нажав на кнопку, либо сосредоточившись на вводе текста comboboxs, существует большая задержка перед отображением списка элементов. Эта задержка становится заметно больше, когда список выбора больше опции.

эта задержка не просто происходит в первый раз, это происходит каждый раз.

поскольку некоторые из выбранных списков в этом проекте очень большие (сотни и сотни элементов), задержка/замораживание браузера недопустима.

может ли кто-нибудь указать мне в правильном направлении, чтобы оптимизировать это? Или даже там, где проблема производительности может быть?

Я считаю, что проблема может быть связана с тем, как скрипт показывает полный список элементы (выполняет ли автозаполнение поиск пустой строки), Есть ли другой способ отображения всех элементов? Возможно, я мог бы построить один случай для отображения всех элементов (поскольку обычно открывается список перед началом ввода), который не выполняет все совпадения регулярных выражений?

здесь это jsfiddle, чтобы возиться с: http://jsfiddle.net/9TaMu/

5 60

5 ответов:

при текущей реализации combobox полный список опустошается и повторно отображается каждый раз, когда вы разворачиваете раскрывающийся список. Также вы застряли с установкой minLength в 0, потому что он должен сделать пустой поиск, чтобы получить полный список.

вот моя собственная реализация, расширяющая виджет автозаполнения. В моих тестах он может обрабатывать списки из 5000 элементов довольно гладко даже на IE 7 и 8. Он отображает полный список только один раз и повторно использует его при нажатии кнопки выпадающего списка. Это также устраняет зависимость параметра minLength = 0. Он также работает с массивами и ajax в качестве источника списка. Кроме того, если у вас есть несколько больших списков, инициализация виджета добавляется в очередь, чтобы он мог работать в фоновом режиме, а не замораживать браузер.

<script>
(function($){
    $.widget( "ui.combobox", $.ui.autocomplete, 
        {
        options: { 
            /* override default values here */
            minLength: 2,
            /* the argument to pass to ajax to get the complete list */
            ajaxGetAll: {get: "all"}
        },

        _create: function(){
            if (this.element.is("SELECT")){
                this._selectInit();
                return;
            }

            $.ui.autocomplete.prototype._create.call(this);
            var input = this.element;
            input.addClass( "ui-widget ui-widget-content ui-corner-left" );

            this.button = $( "<button type='button'>&nbsp;</button>" )
            .attr( "tabIndex", -1 )
            .attr( "title", "Show All Items" )
            .insertAfter( input )
            .button({
                icons: { primary: "ui-icon-triangle-1-s" },
                text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "ui-corner-right ui-button-icon" )
            .click(function(event) {
                // close if already visible
                if ( input.combobox( "widget" ).is( ":visible" ) ) {
                    input.combobox( "close" );
                    return;
                }
                // when user clicks the show all button, we display the cached full menu
                var data = input.data("combobox");
                clearTimeout( data.closing );
                if (!input.isFullMenu){
                    data._swapMenu();
                    input.isFullMenu = true;
                }
                /* input/select that are initially hidden (display=none, i.e. second level menus), 
                   will not have position cordinates until they are visible. */
                input.combobox( "widget" ).css( "display", "block" )
                .position($.extend({ of: input },
                    data.options.position
                    ));
                input.focus();
                data._trigger( "open" );
            });

            /* to better handle large lists, put in a queue and process sequentially */
            $(document).queue(function(){
                var data = input.data("combobox");
                if ($.isArray(data.options.source)){ 
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.options.source);
                }else if (typeof data.options.source === "string") {
                    $.getJSON(data.options.source, data.options.ajaxGetAll , function(source){
                        $.ui.combobox.prototype._renderFullMenu.call(data, source);
                    });
                }else {
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.source());
                }
            });
        },

        /* initialize the full list of items, this menu will be reused whenever the user clicks the show all button */
        _renderFullMenu: function(source){
            var self = this,
                input = this.element,
                ul = input.data( "combobox" ).menu.element,
                lis = [];
            source = this._normalize(source); 
            input.data( "combobox" ).menuAll = input.data( "combobox" ).menu.element.clone(true).appendTo("body");
            for(var i=0; i<source.length; i++){
                lis[i] = "<li class=\"ui-menu-item\" role=\"menuitem\"><a class=\"ui-corner-all\" tabindex=\"-1\">"+source[i].label+"</a></li>";
            }
            ul.append(lis.join(""));
            this._resizeMenu();
            // setup the rest of the data, and event stuff
            setTimeout(function(){
                self._setupMenuItem.call(self, ul.children("li"), source );
            }, 0);
            input.isFullMenu = true;
        },

        /* incrementally setup the menu items, so the browser can remains responsive when processing thousands of items */
        _setupMenuItem: function( items, source ){
            var self = this,
                itemsChunk = items.splice(0, 500),
                sourceChunk = source.splice(0, 500);
            for(var i=0; i<itemsChunk.length; i++){
                $(itemsChunk[i])
                .data( "item.autocomplete", sourceChunk[i])
                .mouseenter(function( event ) {
                    self.menu.activate( event, $(this));
                })
                .mouseleave(function() {
                    self.menu.deactivate();
                });
            }
            if (items.length > 0){
                setTimeout(function(){
                    self._setupMenuItem.call(self, items, source );
                }, 0);
            }else { // renderFullMenu for the next combobox.
                $(document).dequeue();
            }
        },

        /* overwrite. make the matching string bold */
        _renderItem: function( ul, item ) {
            var label = item.label.replace( new RegExp(
                "(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + 
                ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong></strong>" );
            return $( "<li></li>" )
                .data( "item.autocomplete", item )
                .append( "<a>" + label + "</a>" )
                .appendTo( ul );
        },

        /* overwrite. to cleanup additional stuff that was added */
        destroy: function() {
            if (this.element.is("SELECT")){
                this.input.remove();
                this.element.removeData().show();
                return;
            }
            // super()
            $.ui.autocomplete.prototype.destroy.call(this);
            // clean up new stuff
            this.element.removeClass( "ui-widget ui-widget-content ui-corner-left" );
            this.button.remove();
        },

        /* overwrite. to swap out and preserve the full menu */ 
        search: function( value, event){
            var input = this.element;
            if (input.isFullMenu){
                this._swapMenu();
                input.isFullMenu = false;
            }
            // super()
            $.ui.autocomplete.prototype.search.call(this, value, event);
        },

        _change: function( event ){
            abc = this;
            if ( !this.selectedItem ) {
                var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( this.element.val() ) + "$", "i" ),
                    match = $.grep( this.options.source, function(value) {
                        return matcher.test( value.label );
                    });
                if (match.length){
                    match[0].option.selected = true;
                }else {
                    // remove invalid value, as it didn't match anything
                    this.element.val( "" );
                    if (this.options.selectElement) {
                        this.options.selectElement.val( "" );
                    }
                }
            }                
            // super()
            $.ui.autocomplete.prototype._change.call(this, event);
        },

        _swapMenu: function(){
            var input = this.element, 
                data = input.data("combobox"),
                tmp = data.menuAll;
            data.menuAll = data.menu.element.hide();
            data.menu.element = tmp;
        },

        /* build the source array from the options of the select element */
        _selectInit: function(){
            var select = this.element.hide(),
            selected = select.children( ":selected" ),
            value = selected.val() ? selected.text() : "";
            this.options.source = select.children( "option[value!='']" ).map(function() {
                return { label: $.trim(this.text), option: this };
            }).toArray();
            var userSelectCallback = this.options.select;
            var userSelectedCallback = this.options.selected;
            this.options.select = function(event, ui){
                ui.item.option.selected = true;
                if (userSelectCallback) userSelectCallback(event, ui);
                // compatibility with jQuery UI's combobox.
                if (userSelectedCallback) userSelectedCallback(event, ui);
            };
            this.options.selectElement = select;
            this.input = $( "<input>" ).insertAfter( select )
                .val( value ).combobox(this.options);
        }
    }
);
})(jQuery);
</script>

Я изменил способ возврата результатов (в источник функция), потому что функция map() выглядит для меня. Он работает быстрее для больших списков выбора (и меньше тоже), но списки с несколькими тысячами вариантов по-прежнему очень медленно. Я профилировал (с помощью функции профиля firebug) исходный и мой измененный код, и время выполнения выглядит следующим образом:

Оригинал: профилирование (372.578 МС, 42307 вызовов)

модифицированных: Профилирования (0.082 МС, 3 вызова)

вот модифицированный код источник функция, вы можете увидеть исходный код в jQuery UI demo http://jqueryui.com/demos/autocomplete/#combobox. там, конечно, может быть больше оптимизации.

source: function( request, response ) {
    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
    var select_el = this.element.get(0); // get dom element
    var rep = new Array(); // response array
    // simple loop for the options
    for (var i = 0; i < select_el.length; i++) {
        var text = select_el.options[i].text;
        if ( select_el.options[i].value && ( !request.term || matcher.test(text) ) )
            // add element to result array
            rep.push({
                label: text, // no more bold
                value: text,
                option: select_el.options[i]
            });
    }
    // send response
    response( rep );
},

надеюсь, что это помогает.

Мне нравится ответ от Берро. Но поскольку он все еще был немного медленным (у меня было около 3000 вариантов в select), я немного изменил его, так что отображаются только первые N совпадающих результатов. Я также добавил элемент в конце, уведомив пользователя о том, что доступны дополнительные результаты, и отменил фокус и выберите события для этого элемента.

здесь изменен код для исходных и выбранных функций и добавлен один для фокуса:

source: function( request, response ) {
    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
    var select_el = select.get(0); // get dom element
    var rep = new Array(); // response array
    var maxRepSize = 10; // maximum response size  
    // simple loop for the options
    for (var i = 0; i < select_el.length; i++) {
        var text = select_el.options[i].text;
        if ( select_el.options[i].value && ( !request.term || matcher.test(text) ) )
            // add element to result array
            rep.push({
                label: text, // no more bold
                value: text,
                option: select_el.options[i]
            });
        if ( rep.length > maxRepSize ) {
            rep.push({
                label: "... more available",
                value: "maxRepSizeReached",
                option: ""
            });
            break;
        }
     }
     // send response
     response( rep );
},          
select: function( event, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    } else {
        ui.item.option.selected = true;
        self._trigger( "selected", event, {
            item: ui.item.option
        });
    }
},
focus: function( event, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    }
},

мы нашли то же самое, однако в конце концов наше решение было поменьше списки!

когда я заглянул в него, это было сочетание нескольких вещей:

1) содержимое списка очищается и перестраивается каждый раз, когда отображается список (или пользователь что-то и начинает фильтровать список). Я думаю, что это в основном неизбежно и довольно важно для того, как работает список (так как вам нужно удалить элементы из списка по порядку для фильтрации работать).

вы можете попробовать изменить его так, чтобы он показывал и скрывал элементы в списке, а не полностью перестраивал его снова, но это будет зависеть от того, как будет построен ваш список.

альтернативой является попытка оптимизировать очистку / построение списка (см. 2. и 3.).

2) есть существенная задержка при очистке списка. Моя теория заключается в том, что это, по крайней мере, партия из-за каждого элемента списка, имеющего данные прилагается (по data() функция jQuery) - я, кажется, помню, что удаление данных, прикрепленных к каждому элементу, существенно ускорило этот шаг.

возможно, вы захотите изучить более эффективные способы удаления дочерних html-элементов, например как сделать jQuery.пустой более чем в 10 раз быстрее. Будьте осторожны с потенциальными утечками памяти, если вы играете с альтернативой empty функции.

в качестве альтернативы вы можете попробовать настроить его так, чтобы данные не прикрепляется к каждому элементу.

3) остальная задержка связана с построением списка - более конкретно список строится с использованием большой цепочки операторов jQuery, например:

$("#elm").append(
    $("option").class("sel-option").html(value)
);

это выглядит довольно красиво, но является довольно неэффективным способом построения html - гораздо более быстрый способ состоит в том, чтобы построить строку html самостоятельно, например:

$("#elm").html("<option class='sel-option'>" + value + "</option>");

посмотреть Производительность строки: анализ для достаточно углубленная статья о наиболее эффективном способе конкатенации строк (что, по сути, и происходит здесь).


вот где проблема, но я честно не знаю, что лучший способ исправить было бы - в итоге мы сократили наш список элементов, так что это не проблема больше.

по адресу 2) и 3) вы вполне можете обнаружить, что производительность списка улучшается до приемлемого уровня, но если нет, то вам нужно будет обратиться 1) и попытаться прийти с альтернативой очистки и повторного построения списка каждый раз, когда он отображается.

удивительно, что функция фильтрации списка (которая включала некоторые довольно сложные регулярные выражения) очень мало влияла на производительность выпадающего списка - вы должны проверить, чтобы убедиться, что вы не сделали что-то глупое, но для нас это не было производительность bottlekneck.

то, что я сделал, я делюсь:

на _renderMenu, Я написал это:

var isFullMenuAvl = false;
    _renderMenu: function (ul, items) {
                        if (requestedTerm == "**" && !isFullMenuAvl) {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                            fullMenu = $(ul).clone(true, true);
                            isFullMenuAvl = true;
                        }
                        else if (requestedTerm == "**") {
                            $(ul).append($(fullMenu[0].childNodes).clone(true, true));
                        }
                        else {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                        }
                    }

это в основном для обслуживания запросов на стороне сервера. Но он может использоваться для локальных данных. Мы сохраняем requestedTerm и проверяем, соответствует ли он ** что означает, что полный поиск меню продолжается. Вы можете заменить "**" С "" если вы ищете полное меню с "строка поиска". Пожалуйста, свяжитесь со мной для любого типа запросов. Это повышает производительность в моем случае по крайней мере 50%.