Combobox jQuery UI Autocomplete очень медленный с большими списками выбора - PullRequest
63 голосов
/ 22 февраля 2011

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

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

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

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

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

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

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

Вот jsfiddle, с которым нужно поиграться:http://jsfiddle.net/9TaMu/

Ответы [ 5 ]

77 голосов
/ 10 июня 2011

В текущей реализации выпадающего списка полный список очищается и перерисовывается при каждом раскрытии раскрывающегося списка.Кроме того, вы застряли с установкой 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's combobox.
                if (userSelectedCallback) userSelectedCallback(event, ui);
            };
            this.options.selectElement = select;
            this.input = $( "<input>" ).insertAfter( select )
                .val( value ).combobox(this.options);
        }
    }
);
})(jQuery);
</script>
19 голосов
/ 20 апреля 2011

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

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

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

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

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

15 голосов
/ 29 июля 2011

Мне нравится ответ от Берро.Но поскольку он все еще был немного медленным (у меня было около 3000 вариантов выбора), я немного изменил его, чтобы отображались только первые 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;
    }
},
11 голосов
/ 22 февраля 2011

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

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

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

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

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

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

Возможно, вы захотите изучить более эффективные способы удаления дочерних html-элементов, например, Как сделать jQuery.empty более чем в 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.

1 голос
/ 10 мая 2013

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...