jQuery UI Autocomplete Combobox bardzo wolny z dużymi listami wyboru

Używam zmodyfikowanej wersji jQuery UI Autocomplete Combobox, jak widać tutaj: http://jqueryui.com/demos/autocomplete/#combobox

ze względu na to pytanie, powiedzmy, że mam dokładnie ten kod ^^^

Podczas otwierania comboboxu, klikając przycisk lub skupiając się na wejściu tekstu comboboxs, występuje duże opóźnienie przed wyświetlaniem listy elementów. Opóźnienie to staje się zauważalnie większe, gdy lista wyboru ma więcej opcji.

To opóźnienie nie pojawia się po prostu za pierwszym razem, zdarza się za każdym razem.

Ponieważ niektóre listy wyboru w tym projekcie są bardzo duże( setki i setki pozycji), opóźnienie / zamrożenie przeglądarki jest niedopuszczalne.

Czy ktoś może wskazać mi właściwy kierunek, aby to zoptymalizować? A nawet gdzie może być problem z wydajnością?

Wydaje mi się, że problem może mieć związek ze sposobem, w jaki skrypt wyświetla pełną listę elementów (czy autouzupełnianie szuka pustego string), czy istnieje inny sposób wyświetlania wszystkich elementów? Być może mógłbym zbudować jednorazową sprawę do wyświetlania wszystkich elementów (jak to jest powszechne, aby otworzyć listę przed rozpoczęciem pisania), które nie wszystkie pasujące regex?

Oto jsfiddle do zabawy: http://jsfiddle.net/9TaMu/

Author: elwyn, 2011-02-22

5 answers

W obecnej implementacji combobox, pełna lista jest opróżniana i ponownie renderowana za każdym razem, gdy rozwijasz rozwijaną listę. Ponadto utknąłeś z ustawieniem minLength na 0, ponieważ musi wykonać puste wyszukiwanie, aby uzyskać pełną listę.

Oto moja własna implementacja rozszerzająca widżet autouzupełniania. W moich testach całkiem sprawnie radzi sobie z listami 5000 pozycji nawet na IE 7 i 8. Renderuje pełną listę tylko raz i wykorzystuje ją ponownie po kliknięciu przycisku rozwijanego. To także usuwa zależność opcji minLength = 0. Działa również z tablicami i ajax jako źródło listy. Również jeśli masz wiele dużych list, inicjalizacja widżetu jest dodawana do kolejki, dzięki czemu może działać w tle, a nie zamrażać przeglądarkę.

<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>
 76
Author: gary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-09 20:55:33

Zmodyfikowałem sposób zwracania wyników (w funkcji source), ponieważ funkcja map () wydawała mi się powolna. Działa szybciej dla dużych list wyboru (i mniejszych), ale Listy z kilkoma tysiącami opcji są nadal bardzo wolne. Mam profilowany (za pomocą funkcji firebug ' s profile) oryginalny i mój zmodyfikowany kod, a czas wykonania przebiega tak:

Original: Profiling (372.578 ms, 42307 wywołań)

Modified: Profiling (0.082 ms, 3 wywołania)

Oto zmodyfikowany kod funkcji source , możesz zobaczyć oryginalny kod w jQuery UI demo http://jqueryui.com/demos/autocomplete/#combobox . z pewnością może być więcej optymalizacji.

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 );
},
Mam nadzieję, że to pomoże.
 19
Author: Berro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-28 15:36:16

Podoba mi się odpowiedź Berro. Ponieważ jednak nadal był trochę powolny (miałem około 3000 opcji w select), zmodyfikowałem go nieco tak, że wyświetlane są tylko pierwsze N dopasowanych wyników. Dodałem również element na końcu powiadamiając użytkownika, że więcej wyników są dostępne I anulowane fokus i wybierz zdarzenia dla tego elementu.

Tutaj jest zmodyfikowany kod źródłowy i wybierz funkcje i dodano jedną dla Fokusa:

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
Author: Peja,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-28 20:37:24

Znaleźliśmy to samo, jednak ostatecznie naszym rozwiązaniem było posiadanie mniejszych list!

Kiedy przyjrzałem się temu, było to połączenie kilku rzeczy:

1) Zawartość listy jest czyszczona i ponownie budowana za każdym razem, gdy lista jest wyświetlana (lub użytkownik wpisuje coś i zaczyna filtrować listę). Myślę, że jest to w większości nieuniknione i dość rdzeń do sposobu działania ListBox (jak trzeba usunąć elementy z listy w celu filtrowania do pracy).

Możesz spróbować zmienić ją tak, aby wyświetlała i ukrywała elementy na liście, a nie całkowicie ją ponownie konstruować, ale zależy to od tego, jak zostanie zbudowana Twoja lista.

Alternatywą jest próba optymalizacji rozliczeń / konstrukcji listy (zob. 2. i 3.).

2) istnieje znaczne opóźnienie przy czyszczeniu listy . Moja teoria jest taka, że jest to co najmniej strona ze względu na każdy element listy z dołączonymi danymi (przez funkcję data() jQuery) - I wydaje się pamiętać, że usunięcie danych dołączonych do każdego elementu znacznie przyspieszyło ten krok.

Warto przyjrzeć się bardziej efektywnym sposobom usuwania potomnych elementów html, na przykład Jak zrobić jQuery.opróżnij ponad 10x szybciej. Uważaj na potencjalne wycieki pamięci, jeśli grasz z alternatywnymi funkcjami empty.

Alternatywnie możesz spróbować dostosować go tak, aby dane nie były dołączane do każdego elementu.

3) reszta opóźnienie wynika z konstrukcji listy - dokładniej lista jest skonstruowana przy użyciu dużego łańcucha instrukcji jQuery, na przykład:

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

Wygląda to ładnie, ale jest to dość nieefektywny sposób konstruowania html - znacznie szybszym sposobem jest samodzielne skonstruowanie ciągu html, na przykład:

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

Zobacz Wydajność strun: Analiza dla dość dogłębnego artykułu na temat najbardziej efektywnego sposobu łączenia strun (co jest zasadniczo tym, co jest dzieje się tutaj).


W tym tkwi problem, ale szczerze mówiąc Nie wiem, jaki byłby najlepszy sposób na naprawienie tego - w końcu skróciliśmy listę przedmiotów, więc nie było już problemu.

Adresując 2) i 3) możesz zauważyć, że wydajność listy poprawia się do akceptowalnego poziomu, ale jeśli nie, musisz adresować 1) i spróbować wymyślić alternatywę dla czyszczenia i ponownego budowania listy za każdym razem, gdy jest wyświetlana.

Zaskakująco funkcja filtrująca listę (która obejmowała dość złożone wyrażenia regularne) miała bardzo mały wpływ na wydajność rozwijanej listy - powinieneś sprawdzić, czy nie zrobiłeś czegoś głupiego, ale dla nas nie była to wydajność bottlekneck.

 11
Author: Justin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-22 04:08:43

To, co zrobiłem, dzielę się:

W _renderMenu napisałem tak:

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

Jest to głównie do serwowania żądań po stronie serwera. Ale może być używany do danych lokalnych. Przechowujemy requestedTerm i sprawdzamy, czy pasuje do **, co oznacza, że trwa pełne wyszukiwanie menu. Możesz zastąpić "**" "", Jeśli szukasz pełnego menu z "bez szukanego ciągu". Proszę o kontakt w przypadku jakichkolwiek pytań. Poprawia wydajność w moim przypadku o co najmniej 50%.

 1
Author: soham,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-01 09:00:20