2011-05-25 13 views
20

Nunca antes tuve que usar funciones de devolución de llamada, así que puede haber cometido un error completamente estúpido. Creo que de alguna manera entiendo el problema aquí, pero no cómo resolverlo.Pase el parámetro adicional a la función de devolución de llamada jQuery getJSON()

Mi código (un poco simplificada) es:

for (var i = 0; i < some_array.length; i++) { 
    var title = some_array[i]; 
    $.getJSON('some.url/' + title, function(data) { 
     do_something_with_data(data, i); 
    } 

Ahora por lo que tengo entendido, esta función anónima sólo serán llamados si getJSON() ha recibido los datos. Pero en este punto, i no tiene el valor que necesitaría. O, en lo que respecta a mi observación, tiene el último valor que tendría después de que se complete el ciclo (¿no debería estar fuera de límites?).

Como resultado, si la matriz tiene un tamaño de 6, do_something_with_data() sería llamado cinco veces con el valor 5.

Ahora pensé, sólo tiene que pasar i a la función anónima

function(data, i) { } 

pero esto no parece ser posible. i no está definido ahora.

Respuesta

45

es necesario comprender lo que un cierrees. En javascript, cuando dentro de una función utiliza una variable definida en un contexto externo (función externa o global), crea un cierre alrededor de esa variable, lo que mantiene la variable instanciada y permite que la función continúe refiriéndose a ella cada vez que se invoca (así como cualquier otra instancia de función con un cierre en el elemento).

Debido a que la variable original todavía se crea una instancia, si cambia el valor de esa variable cualquier lugar en el código, cuando la función se ejecuta después se tendrá el valor cambiado actual, no el valor cuando la función era la primera creado.

Antes de tratar de hacer el trabajo correcto cierre, tenga en cuenta que la declaración de la variable title en repetidas ocasiones en el circuito no funciona (de hecho, se puede pensar de la variable siendo esencialmente izada en el function 's alcance- -a diferencia de algunos otros lenguajes, los bucles for en JavaScript no tienen ámbito, por lo tanto, la variable se declara solo una vez para la función y es no declarada o redeclarada dentro del bucle). Declarar la variable fuera del ciclo debería ayudar a aclararle por qué su código no funciona como era de esperar.

Como es, cuando las devoluciones de llamada se ejecutan, porque tienen un cierre sobre la misma variable i, todos ellos son afectados cuando i incrementos y que todos ellos utilizar la corriente valor de i cuando se ejecutan (que será mal como descubrió, debido a que las devoluciones de llamada ejecutan después de, el ciclo ha terminado de crear las devoluciones de llamada).El código asíncrono (como la respuesta de llamada JSON) no se ejecuta y no se puede ejecutar hasta que todo el código síncrono termina de ejecutarse, por lo que se garantiza que el ciclo se completará antes de que se ejecute alguna vez la devolución de llamada.

Para evitar esto se necesita una nueva función a ejecutar que tiene su propia alcance para que en las devoluciones de llamada declaradas dentro del bucle, hay un nuevo cierre sobre cada valor diferente . Puede hacerlo con una función separada, o simplemente usar una función anónima invocada en el parámetro de devolución de llamada. He aquí un ejemplo:

var title, i; 
for (i = 0; i < some_array.length; i += 1) { 
    title = some_array[i]; 
    $.getJSON(
     'some.url/' + title, 
     (function(thisi) { 
      return function(data) { 
      do_something_with_data(data, thisi); 
      // Break the closure over `i` via the parameter `thisi`, 
      // which will hold the correct value from *invocation* time. 
      }; 
     }(i)) // calling the function with the current value 
    ); 
} 

Para mayor claridad voy a romperlo a cabo en una función separada para que pueda ver lo que está pasando:

function createCallback(item) { 
    return function(data) { 
     do_something_with_data(data, item); 
     // This reference to the `item` parameter does create a closure on it. 
     // However, its scope means that no caller function can change its value. 
     // Thus, since we don't change `item` anywhere inside `createCallback`, it 
     // will have the value as it was at the time the createCallback function 
     // was invoked. 
    }; 
} 

var title, i, l = some_array.length; 
for (i = 0; i < l; i += 1) { 
    title = some_array[i]; 
    $.getJSON('some.url/' + title, createCallback(i)); 
    // Note how this parameter is not a *reference* to the createCallback function, 
    // but the *value that createCallback() returns*, which is itself a function. 
} 

Nota: desde la matriz al parecer sólo tiene títulos en el mismo, podría considerar usar la variable title en lugar de i, que requiere que regrese a some_array. Pero de cualquier manera funciona, sabes lo que quieres.

Una forma potencialmente útil para pensar en esto que la función de devolución de llamada de creación (ya sea el anónimo uno o el createCallback uno) en esencia convierte el valor de la variable de i en thisi variables separadas, a través de cada vez que la introducción de un nuevo funcionar con su propio alcance Quizás podría decirse que "los parámetros rompen los valores de los cierres".

Solo tenga cuidado: esta técnica no funcionará en los objetos sin copiarlos, ya que los objetos son tipos de referencia. Pasarlos simplemente como parámetros no arrojará algo que no pueda cambiarse después de los hechos. Puede duplicar la dirección de una calle todo lo que quiera, pero esto no crea una nueva casa. Debes construir una nueva casa si quieres una dirección que conduzca a algo diferente.

6

Se puede crear un cierre mediante una función inmediata (uno que se ejecuta de inmediato) que devuelve otra función:

for (var i = 0; i < some_array.length; i++) { 
    var title = some_array[i]; 
    $.getJSON('some.url/' + title, (function() { 
     var ii = i; 
     return function(data) { 
      do_something_with_data(data, ii); 
     }; 
    })()); 
} 
+1

+1 Excepto @Chris quiere mantener una referencia a cada 'i'. – Jeremy

+0

D'oh, gracias, me lo perdí. Se agregó en. – patorjk

+0

Intenté adaptar esto, pero ¿dónde ** datos ** obtiene su valor ahora? No está definido para mí ahora. – Chris

1

Crear N cierres y pase el valor de 'i' cada vez, así:

var i, title; 
for (i = 0; i < some_array.length; i++) { 
    title = some_array[i]; 
    $.getJSON('some.url/' + title, (function(i_copy) { 
     return function(data) { 
      do_something_with_data(data, i_copy); 
     }; 
    })(i)); 
} 
+0

Tenga en cuenta que jslint le pedirá que mueva la declaración var fuera del ciclo y que coloque la invocación de la función anónima entre paréntesis, no afuera. – ErikE

0

creo que algunos navegadores tienen problemas con la fabricación de múltiples llamadas asíncronas al mismo tiempo, por lo que podría hacer que se una a la vez:

var i; 
function DoOne(data) 
{ 
    if (i >= 0) 
     do_something_with_data(data, i); 
    if (++i >= some_array.length) 
     return; 
    var title = some_array[i]; 
    $.getJSON('some.url/' + title, DoOne); 
} 

// to start the chain: 
i = -1; 
DoOne(null); 
+2

No use 'async: false'. Hace que el navegador ** completo **, incluido el subproceso de interfaz de usuario, se bloquee mientras dure la solicitud de red. Tus usuarios estarán muy descontentos si has congelado su navegador. – josh3736

+0

Firefox y Chrome simplemente bloquean la pestaña actual, pero, punto, la eliminé de mi respuesta. –

3

Si se puede modificar el servicio en some.url, sería mucho mejor si en lugar de hacer una solicitud HTTP por separado para cada artículo en some_array, simplemente pasa todos los elementos de la matriz en una sola solicitud HTTP.

$.getJSON('some.url', { items: some_array }, callback); 

Su matriz será serializada JSON y POSTed en el servidor. Suponiendo some_array es una matriz de cadenas, la solicitud se verá así:

POST some.url HTTP/1.1 
... 

{'items':['a','b','c', ... ]} 

Su guión servidor entonces debe deserializar la solicitud JSON desde el cuerpo de la petición y el bucle sobre cada elemento de la matriz items, devolviendo un JSON serializado conjunto de respuestas.

HTTP/1.1 200 OK 
... 

{'items':[{id:0, ... }, {id:1, ... }, ... ]} 

(O cualquier dato que sea que está regresando.) Si los elementos de respuesta están en el mismo orden que los elementos de solicitud, es fácil de pieza cosas de nuevo juntos. En su devolución de llamada exitosa, simplemente haga coincidir el índice del artículo con el índice some_array.Poniendo todo junto:

$.getJSON('some.url', { items: some_array }, function(data) { 
    for (var i = 0; i < data.items.length; i++) { 
     do_something_with_data(data.items[i], i); 
    } 
}); 

Por 'de dosificación arriba' sus peticiones en una sola petición HTTP como esto, usted significativamente mejorar el rendimiento. Tenga en cuenta que si cada viaje de ida y vuelta de red lleva al menos 200 ms, con 5 elementos, está buscando un retraso de 1 segundo como mínimo. Al solicitarlos todos a la vez, el retraso de la red se mantiene constante en 200 ms. (Obviamente, con solicitudes más grandes, la ejecución del script del servidor y los tiempos de transferencia de red entrarán en juego, pero el rendimiento seguirá siendo un orden de una magnitud mejor que si emite una solicitud HTTP por separado para cada elemento.)

+1

Esos son buenos puntos. Probablemente ES MEJOR combinar las solicitudes. – ErikE

0

Tenía exactamente el mismo problema que el OP pero lo resolvió de otra manera. Reemplacé mi bucle de JavaScript por 'for' con un jQuery $ .each que para cada iteración llama a una función que creo que supera el problema de la 'devolución de llamada'. Y combiné mis matrices de datos externos en un objeto JavaScript para poder hacer referencia tanto al parámetro que estaba pasando en la URL JSON como al otro campo en el mismo elemento de ese objeto. Mis elementos de objeto salieron de una tabla de base de datos mySQL usando PHP.

var persons = [ 
{ Location: 'MK6', Bio: 'System administrator' }, 
{ Location: 'LU4', Bio: 'Project officer' }, 
{ Location: 'B37', Bio: 'Renewable energy hardware installer' }, 
{ Location: 'S23', Bio: 'Associate lecturer and first hardware triallist' }, 
{ Location: 'EH12', Bio: 'Associate lecturer with a solar PV installation' } 
]; 

function initMap() { 
    var map = new google.maps.Map(document.getElementById('map_canvas'), { 
    center: startLatLon, 
    minZoom: 5, 
    maxZoom: 11, 
    zoom: 5 
    }); 
    $.each(persons, function(x, person) { 
    $.getJSON('http://maps.googleapis.com/maps/api/geocode/json?address=' + person.Location, null, function (data) { 
     var p = data.results[0].geometry.location; 
     var latlng = new google.maps.LatLng(p.lat, p.lng); 
     var image = 'images/solarenergy.png'; 
     var marker = new google.maps.Marker({ 
     position: latlng, 
     map: map, 
     icon: image, 
     title: person.Bio 
     }); 
     google.maps.event.addListener(marker, "click", function (e) { 
     document.getElementById('info').value = person.Bio; 
     }); 
    }); 
    }); 
} 
Cuestiones relacionadas