jQuery UI 自动完成组合框对于大型选择列表非常慢

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5073612/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 18:31:18  来源:igfitidea点击:

jQuery UI Autocomplete Combobox Very Slow With Large Select Lists

jqueryperformancejquery-uicomboboxautocomplete

提问by elwyn

I'm using a modified version of the jQuery UI Autocomplete Combobox, as seen here: http://jqueryui.com/demos/autocomplete/#combobox

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

For the sake of this question, let's say I have exactly that code ^^^

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

When opening the combobox, either by clicking the button or focusing on the comboboxs text input, there is a large delay before showing the list of items. This delay gets noticeably larger when the select list has more options.

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

This delay doesn't just occur the first time either, it happens every time.

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

As some of the select lists on this project are very large (hundreds and hundreds of items), the delay/browser freezing up is unacceptable.

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

Can anyone point me in the right direction to optimise this? Or even where the performance problem may be?

任何人都可以指出我正确的方向来优化它吗?甚至性能问题可能在哪里?

I believe the issue may be to do with the way the script shows the full list of items (does an autocomplete search for an empty string), is there another way to display all items? Perhaps I could build a one off case for displaying all items (as it is common to open the list before starting to type) that doesn't do all the regex matching?

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

Here is a jsfiddle to fiddle with: http://jsfiddle.net/9TaMu/

这是一个 jsfiddle 来摆弄:http: //jsfiddle.net/9TaMu/

回答by gary

With the current combobox implementation, the full list is emptied and re-rendered every time you expand the dropdown. Also you are stuck with setting the minLength to 0, because it has to do an empty search to get the full list.

使用当前的组合框实现,每次展开下拉列表时都会清空并重新呈现完整列表。此外,您还坚持将 minLength 设置为 0,因为它必须进行空搜索才能获得完整列表。

Here is my own implementation extending the autocomplete widget. In my tests it can handle lists of 5000 items pretty smoothly even on IE 7 and 8. It renders the full list just once, and reuses it whenever the dropdown button is clicked. This also removes the dependence of the option minLength = 0. It also works with arrays, and ajax as list source. Also if you have multiple large list, the widget initialization is added to a queue so it can run in the background, and not freeze the browser.

这是我自己的扩展自动完成小部件的实现。在我的测试中,即使在 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></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>

回答by Berro

I've modified the way the results are returned (in the sourcefunction) because the map() function seemed slow to me. It runs faster for large select lists (and smaller too), but lists with several thousands of options are still very slow. I've profiled (with firebug's profile function) the original and my modified code, and the execution time goes like this:

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

Original: Profiling (372.578 ms, 42307 calls)

Modified: Profiling (0.082 ms, 3 calls)

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

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

Here is the modified code of the sourcefunction, you can see the original code at the jquery ui demo http://jqueryui.com/demos/autocomplete/#combobox. There can certainly be more optimization.

下面是源码函数的修改后的代码,原代码可以在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 );
},

Hope this helps.

希望这可以帮助。

回答by Peja

I like the answer from Berro. But because it was still a bit slow (I had about 3000 options in select), i modified it slightly so that only first N matching results are displayed. I also added an item at the end notifying the user that more results are available and canceled focus and select events for that item.

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

Here is modified code for source and select functions and added one for focus:

这是源和选择函数的修改代码,并添加了一个焦点:

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

回答by Justin

We found the same thing, however in the end our solution was to have smaller lists!

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

When I looked into it it was a combination of several things:

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

1) The contents of the list box is cleared and re-built every time the list box is shown(or the user types something in and starts to filter the list). I think that this is mostly unavoidable and fairly core to the way the list box works (as you need to remove items from the list in order for filtering to work).

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

You could try changing it so that it shows and hides items in the list rather than completely re-constructing it again, but it would depend on how your list is constructed.

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

The alternative is to try and optimise the clearing / construction of the list (see 2. and 3.).

另一种方法是尝试优化列表的清理/构建(参见 2. 和 3.)。

2) There is a substantial delay when clearing the list. My theory is that this is at least party due to every list item having data attached (by the data()jQuery function) - I seem to remember that removing the data attached to each element substantially sped up this step.

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

You might want to look into more efficient ways of removing child html elements, for example How To Make jQuery.empty Over 10x Faster. Be careful of potentially introducing memory leaks if you play with alternative emptyfunctions.

您可能想研究更有效的方法来删除子 html 元素,例如How To Make jQuery.empty Over 10x Faster。如果您使用替代empty函数,请小心可能引入内存泄漏。

Alternatively you might want to try to tweak it so that data isn't attached to each element.

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

3) The rest of the delay is due to the construction of the list- more specifically the list is constructed using a large chain of jQuery statements, for example:

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

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

This looks pretty, but is a fairly inefficient way of constructing html - a much quicker way is to construct the html string yourself, for example:

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

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

See String Performance: an Analysisfor a fairly in-depth article on the most efficient way of concatenating strings (which is essentially what is going on here).

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



Thats where the problem is, but I honestly don't know what the best way of fixing it would be - in the end we shortened our list of items so it wasn't a problem any more.

这就是问题所在,但老实说,我不知道修复它的最佳方法是什么 - 最后我们缩短了我们的项目列表,所以它不再是一个问题。

By addressing 2) and 3) you may well find that the performance of the list improves to an acceptable level, but if not then you will need to address 1) and try to come up with an alternative to clearing and re-building the list every time it is displayed.

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

Surprisingly the function filtering the list (which involved some fairly complex regular expressions) had very little effect on the performance of the drop down - you should check to make sure that you have not done something silly, but for us this wasn't the performance bottlekneck.

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

回答by soham

What I have done I am sharing:

我所做的我正在分享:

In the _renderMenu, I've written this:

在 中_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);
                            });
                        }
                    }

This is mainly for server side request serving. But it can used for local data. We are storing requestedTerm and checking if it matches with **which means full menu search is going on. You can replace "**"with ""if you are searching full menu with "no search string". Please reach me for any type of queries. It improves performance in my case for at least 50%.

这主要用于服务器端请求服务。但它可以用于本地数据。我们正在存储requestedTerm 并检查它是否匹配,**这意味着完整的菜单搜索正在进行中。如果您使用“无搜索字符串”搜索完整菜单"**"""则可以替换为。请联系我进行任何类型的查询。在我的情况下,它至少提高了 50% 的性能。