jQuery UI自动完成组合框很慢,选择列表很大

时间:2011-02-22 03:10:05

标签: jquery performance jquery-ui combobox autocomplete

我正在使用jQuery UI Autocomplete Combobox的修改版本,如下所示: http://jqueryui.com/demos/autocomplete/#combobox

为了这个问题,让我说我有完全相同的代码^^^

打开组合框时,通过单击按钮或聚焦组合框文本输入,在显示项目列表之前会有很大的延迟。当选择列表有更多选项时,此延迟会明显变大。

这种延迟不仅在第一次发生,而且每次都会发生。

由于此项目中的某些选择列表非常大(数百和数百项),延迟/浏览器冻结是不可接受的。

有人能指出我正确的方向来优化这个吗?或者甚至可能出现性能问题?

我认为问题可能与脚本显示完整项目列表的方式有关(自动完成搜索空字符串),是否有其他方式显示所有项目?也许我可以建立一个关闭显示所有项目的情况(因为在开始输入之前打开列表很常见)并不能完成所有的正则表达式匹配?

这是一个摆弄的小提琴手: http://jsfiddle.net/9TaMu/

5 个答案:

答案 0 :(得分:77)

使用当前的组合框实现,每次展开下拉列表时都会清空并重新呈现完整列表。此外,你仍然坚持将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>

答案 1 :(得分:19)

我已经修改了返回结果的方式(在函数中),因为map()函数对我来说似乎很慢。它对于大型选择列表运行得更快(也更小),但具有数千个选项的列表仍然非常慢。 我已经描述了(使用firebug的profile函数)原始代码和修改后的代码,执行时间如下:

  

原文:分析(372.578毫秒,42307电话)

     

修改:分析(0.082毫秒,3个电话)

以下是功能的修改代码,您可以在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 );
},

希望这有帮助。

答案 2 :(得分:15)

我喜欢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;
    }
},

答案 3 :(得分:11)

我们发现了相同的事情,但最终我们的解决方案是拥有更小的列表!

当我调查它时,它是几件事的组合:

1)每次显示列表框时清除并重新构建列表框的内容(或用户输入内容并开始过滤列表)。我认为这对于列表框的工作方式来说几乎是不可避免的并且相当核心(因为您需要从列表中删除项目以便过滤才能工作)。

您可以尝试更改它,以便它显示和隐藏列表中的项目,而不是再次完全重新构建它,但这取决于列表的构建方式。

另一种方法是尝试优化清单的清除/构建(见2.和3.)。

2)清除列表时存在实质性延迟。我的理论是,至少是派对,因为每个列表项都附加了数据(通过data() jQuery函数) - 我似乎记得删除附加到每个元素的数据大大加快了这一步。

您可能希望研究一种更有效的方法来删除子html元素,例如How To Make jQuery.empty Over 10x Faster。如果您使用其他empty函数,请注意可能引入内存泄漏。

或者,您可能希望尝试调整它,以便数据不会附加到每个元素。

3)延迟的其余部分是由于列表的构造 - 更具体地说,列表是使用大量的jQuery语句构造的,例如:

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

这看起来很漂亮,但是构建html的效率相当低 - 更快的方法是自己构造html字符串,例如:

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

请参阅 String Performance: an Analysis以获得有关串联字符串的最有效方法的相当深入的文章(这基本上就是这里发生的事情)。


问题出在哪里,但老实说我不知道​​解决问题的最佳方法是什么 - 最后我们缩短了我们的项目清单,这样就不再是问题了。

通过解决2)和3)您可能会发现列表的性能提高到可接受的水平,但如果没有,那么您将需要解决1)并尝试提出清除和重新选择每次显示时都会构建列表。

令人惊讶的是,过滤列表的函数(涉及一些相当复杂的正则表达式)对下拉列表的性能几乎没有影响 - 你应该检查以确保你没有做过愚蠢的事情,但对我们来说这不是'性能瓶颈。

答案 4 :(得分:1)

我正在分享的内容:

_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%的性能。