jQuery UI自动完成combobox非常慢,大select列表

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

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

打开combobox时,无论是通过单击button还是关注combobox文本input,在显示项目列表之前都有很长的延迟时间。 当select列表有更多的选项时,这个延迟明显变大。

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

由于这个项目中的一些select列表非常大(数百和数百项),所以延迟/浏览器冻结是不可接受的。

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

我相信这个问题可能是脚本显示完整列表的方式(自动完成search空string)的方式,是否有另一种方式来显示所有项目? 也许我可以build立一个一次性的情况下显示所有项目(因为它是打开列表之前开始打字是常见的),不做所有的正则expression式匹配?

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

使用当前的combobox实现,每当您展开下拉菜单时,完整列表都将被清空并重新呈现。 你也坚持设置minLength为0,因为它必须做一个空的search来获得完整的列表。

这是我自己的实现扩展自动完成小部件。 在我的testing中,即使在IE 7和8上,它也可以很顺利地处理5000个项目的列表。它只显示一次完整列表,并在下拉button被点击时重新使用。 这也消除了选项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, ie 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> 

我修改了结果返回的方式(在函数中),因为map()函数对我来说似乎很慢。 对于大型的select列表,运行速度更快(而且也更小),但是具有几千个选项的列表仍然非常慢。 我已经configuration(与萤火虫的configuration文件function)原来的和我修改后的代码,执行时间是这样的:

原文:Profiling(372.578 ms,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 ); }, 

希望这可以帮助。

问候。

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

这里是源代码和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; } }, 

我们find了同样的东西,但最终我们的解决scheme是有更小的列表!

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

1)列表框的内容在每次显示列表框 (或者用户键入内容并开始过滤列表)时被清除并重新构build 。 我认为这对于列表框的工作方式来说基本上是不可避免的,也是相当核心的(因为您需要从列表中删除项目才能进行过滤)。

您可以尝试更改它,以便它显示并隐藏列表中的项目,而不是完全重新构build它,但这取决于您的列表是如何构build的。

另一种方法是尝试优化清单的清理/build设(见2.和3.)。

2)清单清单有很大的延误 。 我的理论是,这至less是由于每个列表项有数据附加(通过data() jQuery函数) – 我似乎记得删除每个元素附加的数据大大加快了这一步。

您可能想要查看删除子html元素的更有效方法,例如如何使jQuery.empty速度提高10倍以上 。 如果使用替代empty函数,请小心可能引入内存泄漏。

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

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

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

这看起来很漂亮,但是构buildhtml的效率相当低 – 更快捷的方法是自己构造htmlstring,例如:

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

请参阅string性能:分析有关连接string的最有效方式的相当深入的文章(这基本上是这里所发生的)。


那就是问题出在哪里,但是我真的不知道最好的解决办法是什么 – 最后我们缩短了我们的物品清单,所以这不再是问题。

通过解决2)和3)你可能会发现列表的性能提高到可以接受的水平,但是如果没有,那么你将需要解决1),并试图提出一个替代清除和重build列表每次显示。

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

我已经做了我分享:

_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并检查它是否与**匹配,这意味着完整的菜单search正在进行。 如果您使用“无searchstring”search完整菜单,则可以用"**"replace"**" ”。 请联系我任何types的查询。 它提高了我的情况至less50%的性能。