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

elw*_*wyn 62 performance jquery combobox jquery-ui autocomplete

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

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

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

这种延迟不仅仅发生在第一次,它每次都会发生.

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

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

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

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

gar*_*ary 78

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

  • @dallin上面的脚本依赖于jquery-ui 1.8.x,它需要一些小的改动来适用于1.9.x. 自从我上次工作以来已经有一段时间了,但我在这里发布了代码https://github.com/garyzhu/jquery.ui.combobox我没有用最新的jquery-ui彻底测试它,只是修复了明显的javascript错误. (6认同)
  • 优秀!可惜我只能投票一次! (3认同)

小智 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 );
},
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.


小智 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;
    }
},
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很棒的解决方案.我继续并扩展了自动完成的_renderMenu事件,因为在asp.net中使用AutoPostback下拉回发它. (2认同)

Jus*_*tin 11

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

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

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

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

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

2)清除列表时会有很长的延迟.我的理论是,由于每个列表项都附加了数据(通过data()jQuery函数),这至少是派对- 我似乎记得删除附加到每个元素的数据大大加快了这一步.

您可能希望研究更有效的方法来删除子html元素,例如如何使jQuery.empty超过10倍.如果您使用其他empty功能,请注意可能引入内存泄漏.

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

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

$("#elm").append(
    $("option").class("sel-option").html(value)
);
Run Code Online (Sandbox Code Playgroud)

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

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

请参阅 字符串性能:一篇关于连接字符串最有效方法的相当深入的文章的分析(这基本上就是这里发生的事情).


问题出在那里,但老实说我不知道​​解决问题的最佳方法是什么 - 最后我们缩短了我们的项目清单,所以它不再是问题.

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

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