ejemplo jquery performance jquery-ui combobox autocomplete

ejemplo - jquery autocomplete select



jQuery UI Autocompletar Combobox muy lento con grandes listas seleccionadas (5)

Con la implementación actual de combobox, la lista completa se vacía y vuelve a generar cada vez que expande el menú desplegable. También está atascado con la configuración de minLength en 0, porque tiene que hacer una búsqueda vacía para obtener la lista completa.

Aquí está mi propia implementación extendiendo el widget de autocompletar. En mis pruebas, puede manejar listas de 5000 elementos sin problemas incluso en IE 7 y 8. Muestra la lista completa solo una vez, y la reutiliza cuando se hace clic en el botón desplegable. Esto también elimina la dependencia de la opción minLength = 0. También funciona con arrays y ajax como fuente de lista. Además, si tiene varias listas grandes, la inicialización del widget se agrega a una cola para que pueda ejecutarse en segundo plano y no congelar el navegador.

<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>

Estoy usando una versión modificada del Combobox de autocompletado de la interfaz de usuario de jQuery, como se ve aquí: http://jqueryui.com/demos/autocomplete/#combobox

Por el bien de esta pregunta, digamos que tengo exactamente ese código ^^^

Al abrir el cuadro combinado, ya sea haciendo clic en el botón o centrándose en la entrada de texto de los cuadros de diálogo, hay un gran retraso antes de mostrar la lista de elementos. Este retraso se hace notablemente mayor cuando la lista de selección tiene más opciones.

Esta demora no solo ocurre la primera vez, sucede siempre.

Como algunas de las listas de selección de este proyecto son muy grandes (cientos y cientos de elementos), la congelación del retraso / navegador es inaceptable.

¿Alguien puede señalarme en la dirección correcta para optimizar esto? O incluso donde el problema de rendimiento puede ser?

Creo que el problema puede tener que ver con la forma en que el guión muestra la lista completa de elementos (¿una búsqueda autocompletada para una cadena vacía), hay alguna otra manera de mostrar todos los elementos? Tal vez podría construir un único caso para mostrar todos los elementos (ya que es común abrir la lista antes de comenzar a escribir) que no hace todas las correspondencias de expresiones regulares.

Aquí hay un jsfiddle para jugar con: http://jsfiddle.net/9TaMu/


Encontramos lo mismo, sin embargo, al final, nuestra solución fue tener listas más pequeñas.

Cuando lo analicé, fue una combinación de varias cosas:

1) El contenido del cuadro de lista se borra y se vuelve a generar cada vez que se muestra el cuadro de lista (o el usuario escribe algo y comienza a filtrar la lista). Creo que esto es casi inevitable y bastante básico para la forma en que funciona el cuadro de lista (ya que es necesario eliminar elementos de la lista para que el filtrado funcione).

Podría intentar cambiarlo para que muestre y oculte los elementos en la lista en lugar de volver a construirlo por completo nuevamente, pero dependerá de cómo se construya su lista.

La alternativa es tratar de optimizar la limpieza / construcción de la lista (ver 2. y 3.).

2) Hay un retraso sustancial al borrar la lista . Mi teoría es que esto es al menos parte debido a que cada elemento de la lista tiene datos adjuntos (por la función jQuery de data() ). Parece recordar que la eliminación de los datos adjuntos a cada elemento aceleró sustancialmente este paso.

Es posible que desee buscar formas más eficientes de eliminar elementos html secundarios, por ejemplo, Cómo hacer jQuery.empty más de 10 veces más rápido . Tenga cuidado de introducir posibles fugas de memoria si juega con funciones empty alternativas.

Alternativamente, puede intentar ajustarlo para que los datos no estén adjuntos a cada elemento.

3) El resto del retraso se debe a la construcción de la lista ; más específicamente, la lista se construye utilizando una gran cadena de declaraciones jQuery, por ejemplo:

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

Esto se ve bastante, pero es una forma bastante ineficiente de construir html, una forma mucho más rápida es construir la cadena html usted mismo, por ejemplo:

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

Ver String Performance: un análisis para un artículo bastante profundo sobre la forma más eficiente de concatenar cadenas (que es esencialmente lo que está sucediendo aquí).

Ahí está el problema, pero sinceramente no sé cuál sería la mejor manera de solucionarlo, al final acortamos nuestra lista de elementos para que ya no fuera un problema.

Al abordar 2) y 3), es posible que el rendimiento de la lista mejore a un nivel aceptable, pero si no, tendrá que abordar 1) y tratar de encontrar una alternativa para borrar y reconstruir la lista cada vez que se muestra.

Sorprendentemente, la función de filtrado de la lista (que incluía expresiones regulares bastante complejas) tuvo muy poco efecto en el rendimiento del menú desplegable. Debería verificar que no haya hecho algo tonto, pero para nosotros este no fue el rendimiento. cuello de botella.


Lo que he hecho lo estoy compartiendo:

En _renderMenu , he escrito esto:

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

Esto es principalmente para la porción de solicitud del lado del servidor. Pero puede usarse para datos locales. Estamos almacenando RequestTerm y comprobando si coincide con ** que significa que la búsqueda de menú completo está en marcha. Puede reemplazar "**" con "" si está buscando el menú completo con "sin cadena de búsqueda". Por favor, comuníquese conmigo para cualquier tipo de consulta. Mejora el rendimiento en mi caso al menos un 50%.


Me gusta la respuesta de Berro. Pero debido a que todavía era un poco lento (tenía alrededor de 3000 opciones en la selección), lo modifiqué ligeramente para que solo se muestren los primeros resultados de N coincidencia. También agregué un artículo al final que notifica al usuario que hay más resultados disponibles y que se canceló el enfoque y seleccione eventos para ese artículo.

Aquí está el código modificado para las funciones de origen y selección y se agregó uno para el foco:

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


Modifiqué la forma en que se devuelven los resultados (en la función fuente ) porque la función map () me pareció lenta. Funciona más rápido para listas de selección grandes (y más pequeñas también), pero las listas con varios miles de opciones siguen siendo muy lentas. He perfilado (con la función de perfil de firebug) el código original y mi código modificado, y el tiempo de ejecución es el siguiente:

Original: Perfilado (372.578 ms, 42307 llamadas)

Modificado: Perfilado (0.082 ms, 3 llamadas)

Aquí está el código modificado de la función fuente , puede ver el código original en la demostración de jquery ui http://jqueryui.com/demos/autocomplete/#combobox . Sin duda puede haber más optimización.

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

Espero que esto ayude.