【问题标题】:jQuery UI Autocomplete Combobox Very Slow With Large Select Lists带有大型选择列表的 jQuery UI 自动完成组合框非常慢
【发布时间】:2025-12-02 00:25:01
【问题描述】:

我正在使用 jQuery UI 自动完成组合框的修改版本,如下所示: http://jqueryui.com/demos/autocomplete/#combobox

为了这个问题,假设我有那个代码^^^

打开组合框时,无论是通过单击按钮还是专注于组合框文本输入,在显示项目列表之前都会有很大的延迟。 This delay gets noticeably larger when the select list has more options.

这种延迟也不是第一次发生,而是每次都会发生。

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

谁能指出我优化这个的正确方向?甚至性能问题可能出在哪里?

我认为问题可能与脚本显示完整项目列表的方式有关(是否自动完成搜索空字符串),是否有另一种显示所有项目的方法?也许我可以构建一个一次性案例来显示所有不进行所有正则表达式匹配的项目(因为通常在开始输入之前打开列表)?

这是一个可以摆弄的 jsfiddle: http://jsfiddle.net/9TaMu/

【问题讨论】:

  • 在创建小部件之前执行所有正则表达式和操作,您可能会看到最大的速度提升,因此在使用小部件时只执行简单的数组/对象查找。

标签: jquery performance jquery-ui combobox autocomplete


【解决方案1】:

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

【讨论】:

  • 恒星!这真的为我加快了速度。谢谢!
  • 我想使用你的实现,因为它很完美,但是当我尝试它并单击按钮时,没有任何反应!没有菜单出现!自动完成功能仍然有效。知道为什么吗?会不会是因为 jquery ui 的更新?
  • @dallin 上面的脚本依赖于 jquery-ui 1.8.x,它需要一些小的改动才能在 1.9.x 上工作。自从我上次工作以来已经有一段时间了,但是我在这里发布了代码github.com/garyzhu/jquery.ui.combobox 我没有用最新的 jquery-ui 彻底测试它,只是修复了明显的 javascript 错误。
  • 感谢 Gary 的解决方案。但是,我们有几个问题。不是大问题,但虽然问题要解决。你有更新的版本吗?
  • @gary 或任何人都可以为上述解决方案提供 jsfiddle 链接?
【解决方案2】:

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

原文:Profiling(372.578 毫秒,42307 次调用)

修改:分析(0.082 毫秒,3 次调用)

这里是源码函数的修改代码,原代码可以在jquery ui demohttp://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 );
},

希望这会有所帮助。

【讨论】:

  • 当对多个下拉列表使用相同的实现时,此解决方案总是返回相同的结果集。
  • 也许 jquery-ui 的源代码在过去 5 年中发生了变化,但是“select.get(0);”需要是“this.element.get(0);”工作。
  • 好答案,但 for 循环必须有 select_el.options.length 而不是 select_el.length。我编辑了代码。
  • 我用这个替换了我的“source:”代码行,我的自动完成功能甚至没有出现。
【解决方案3】:

我喜欢 Berro 的回答。但是因为它还是有点慢(我有大约 3000 个选项可供选择),所以我稍微修改了一下,只显示前 N 个匹配结果。 我还在最后添加了一个项目,通知用户有更多结果可用,并取消了该项目的焦点和选择事件。

这里是 source 和 select 函数的修改代码,并添加了一个焦点:

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

【讨论】:

  • 当然给出的解决方案是不同的,但你的给出了最好的性能。谢谢!
  • 这是一个很棒的解决方案。我继续并扩展了自动完成的 _renderMenu 事件,因为在 asp.net 中使用 AutoPostback 下拉菜单可以回发。
  • @iMatoria Praveen 先生,今天我对您添加的文件进行了一些更改,也很高兴在这篇文章中看到您...而且您在 Audit Expense 中的 Jquery 工作非常棒...目前我我正在研究它并通过您编写的代码学到很多东西..:).. 感谢您给我机会在这里工作..但不幸的是,您已经离开了这里...如果您在这里,学习会更加丰富... :)
  • @MayankPathak - 感谢您的赞赏。
  • 嗨 Peja,您的解决方案对我有用,但经过多次搜索并单击组合框后,它再次冻结浏览器有什么想法吗?
【解决方案4】:

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

当我研究它时,它是几件事的结合:

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) 并尝试提出一种替代方法来清除和重新每次显示时构建列表。

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

【讨论】:

  • 感谢您的全面回答!这给了我明天要做的事情:) 我很想缩短列表,我不认为下拉列表完全适合这么大的列表,但是我不确定这是否可能。
  • @elwyn - 告诉我进展如何 - 这是我真正想要解决的问题之一,但我们只是没有时间去做。
  • 除了 Berro 发布的内容之外,还有其他人优化过其他内容吗? :)
【解决方案5】:

我所做的我正在分享:

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

【讨论】:

    最近更新 更多