it-swarm.com.ru

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

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

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

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

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

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

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

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

Вот jsfiddle для игры: http://jsfiddle.net/9TaMu/

62
elwyn

В текущей реализации выпадающего списка полный список очищается и перерисовывается при каждом раскрытии раскрывающегося списка. Кроме того, вы застряли с установкой 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>
77
gary

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

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

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

Вот модифицированный код функции источника , вы можете увидеть исходный код в демонстрационной версии jquery ui http://jqueryui.com)./демки/автозаполнение/# выпадающие . Там может быть больше оптимизации.

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 );
},

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

19
Berro

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

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

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

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.

11
Justin

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

В _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%.

1
soham