Подтвердить что ты не робот

JQuery UI Autocomplete Combobox Очень медленно с большими списками выбора

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

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

При открытии выпадающего списка либо нажатием кнопки, либо фокусировкой на текстовом входе со списком, перед отображением списка элементов происходит большая задержка. Эта задержка становится заметно больше, когда список выбора имеет больше опций.

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

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

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

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

Вот jsfiddle, чтобы играть с: http://jsfiddle.net/9TaMu/

4b9b3361

Ответ 1

При текущей реализации 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>$1</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 combobox.
                if (userSelectedCallback) userSelectedCallback(event, ui);
            };
            this.options.selectElement = select;
            this.input = $( "<input>" ).insertAfter( select )
                .val( value ).combobox(this.options);
        }
    }
);
})(jQuery);
</script>

Ответ 2

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

Оригинал: Профилирование (372,578 мс, 42307 вызовов)

Модифицировано: профилирование (0,082 мс, 3 вызова)

Вот модифицированный код функции source, вы можете увидеть исходный код в 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 );
},

Надеюсь, что это поможет.

С уважением.

Ответ 3

Мне нравится ответ Берро. Но поскольку это было все еще немного медленным (у меня было около 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;
    }
},

Ответ 4

Мы нашли то же самое, но в итоге наше решение состояло в том, чтобы иметь меньшие списки!

Когда я посмотрел на него, это было сочетание нескольких вещей:

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Ответ 5

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

В _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);
                            });
                        }
                    }

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