web-dev-qa-db-ja.com

大きな選択リストでjQuery UIオートコンボコンボボックスが非常に遅い

ここに見られるように、jQuery UI Autocomplete Comboboxの修正版を使用しています: http://jqueryui.com/demos/autocomplete/#combobox

この質問のために、正確にそのコードがあるとしましょう^^^

ボタンをクリックするか、コンボボックスのテキスト入力にフォーカスしてコンボボックスを開くと、アイテムのリストが表示されるまでに大きな遅延が発生します。選択リストに多くのオプションがある場合、この遅延は著しく大きくなります。

この遅延は、初めて発生するだけでなく、毎回発生します。

このプロジェクトの一部の選択リストは非常に大きいため(数百および数百のアイテム)、遅延/ブラウザのフリーズアップは受け入れられません。

これを最適化するために誰かが正しい方向に向けることができますか?または、パフォーマンスの問題がどこにあるのでしょうか?

問題は、スクリプトがアイテムの完全なリストを表示する方法(空の文字列のオートコンプリート検索を行う)に関係していると思われますが、すべてのアイテムを表示する別の方法はありますか?おそらく、すべてのアイテムを表示するためのワンオフケースを作成して(入力を開始する前にリストを開くのが一般的である)、すべての正規表現のマッチングを行わないのでしょうか?

ここでjsfiddleをいじります: http://jsfiddle.net/9TaMu/

62
elwyn

現在のコンボボックスの実装では、ドロップダウンを展開するたびに完全なリストが空になり、再レンダリングされます。また、完全なリストを取得するには空の検索を行う必要があるため、minLengthを0に設定する必要があります。

オートコンプリートウィジェットを拡張する独自の実装を次に示します。私のテストでは、IE 7および8でも5000アイテムのリストを非常にスムーズに処理できます。完全なリストを一度だけレンダリングし、ドロップダウンボタンがクリックされるたびに再利用します。オプション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>
78
gary

Map()関数が遅いように思えたため、結果が返される方法を変更しました(source関数で)。選択リストが大きい場合(および小さい場合)は高速に実行されますが、数千のオプションを持つリストは依然として非常に低速です。元のコードと変更したコードを(firebugのプロファイル関数で)プロファイルしましたが、実行時間は次のようになります。

オリジナル:プロファイリング(372.578ミリ秒、42307コール)

変更:プロファイリング(0.082 ms、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 );
},

お役に立てれば。

19
Berro

ベロの答えが好きです。しかし、それでもまだ少し遅いため(selectに約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関数によるデータを持つすべてのリストアイテムによるパーティーであるということです-各要素に添付されたデータを削除すると、このステップが大幅に高速化されたことを覚えているようです。

jQuery.emptyを10倍以上速くする方法 など、子html要素を削除するより効率的な方法を検討することをお勧めします。代替のempty関数を使用する場合、潜在的にメモリリークを引き起こすことに注意してください。

または、データが各要素に添付されないように微調整してみることもできます。

3)残りの遅延は、リストの構築によるものです-より具体的には、リストはjQueryステートメントの大きなチェーンを使用して構築されます:

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

これはきれいに見えますが、htmlを構築するのにかなり非効率的な方法です。もっと簡単な方法は、html文字列を自分で構築することです。次に例を示します。

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

文字列を連結する最も効率的な方法に関するかなり詳細な記事については、 文字列パフォーマンス:分析 を参照してください(これは基本的にここで行われています)。


それが問題のある場所ですが、正直に言って、それを修正する最善の方法が何であるかわかりません-最後に、問題のないようにアイテムのリストを短くしました。

2)および3)に対処することで、リストのパフォーマンスが許容レベルまで向上することがわかりますが、そうでない場合は1)に対処し、リストをクリアして再構築する代わりの方法を考え出す必要があります。表示されるたびに。

驚くべきことに、リストをフィルタリングする関数(かなり複雑な正規表現を含む)はドロップダウンのパフォーマンスにほとんど影響を与えませんでした-愚かなことをしていないことを確認する必要がありますが、これはパフォーマンスではありませんでしたボトルネック。

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

これは主にサーバー側のリクエスト処理用です。ただし、ローカルデータには使用できます。 requestedTermを保存し、**と一致するかどうかを確認しています。これは、完全なメニュー検索が行われていることを意味します。 「検索文字列なし」でフルメニューを検索する場合は、"**"""に置き換えることができます。どんな種類の質問でも私に連絡してください。私の場合、少なくとも50%パフォーマンスが向上します。

1
soham