2009-05-13 21 views
20

Tengo un objeto Javascript que requiere 2 llamadas a un servidor externo para crear su contenido y hacer algo significativo. El objeto está construido de tal manera que la instancia de una instancia de este hará automáticamente estas 2 llamadas. Las 2 llamadas comparten una función de devolución de llamada común que opera sobre los datos devueltos y luego llama a otro método. El problema es que el siguiente método no debe invocarse hasta que ambos métodos regresen. Aquí está el código que he implementado en la actualidad:Javascript - sincronización después de llamadas asíncronas

foo.bar.Object = function() { 
this.currentCallbacks = 0; 
this.expectedCallbacks = 2; 

this.function1 = function() { 
    // do stuff 
    var me = this; 
    foo.bar.sendRequest(new RequestObject, function(resp) { 
     me.commonCallback(resp); 
     }); 
}; 

this.function2 = function() { 
    // do stuff 
    var me = this; 
    foo.bar.sendRequest(new RequestObject, function(resp) { 
     me.commonCallback(resp); 
     }); 
}; 

this.commonCallback = function(resp) { 
    this.currentCallbacks++; 
    // do stuff 
    if (this.currentCallbacks == this.expectedCallbacks) { 
     // call new method 
    } 
}; 

this.function1(); 
this.function2(); 
} 

Como se puede ver, yo estoy forzando el objeto de continuar después de que ambos han regresado las llamadas utilizando un simple contador para validar que tienen ambos regresaron. Esto funciona, pero parece una implementación realmente pobre. Solo he trabajado con Javascript durante algunas semanas y me pregunto si existe un método mejor para hacer lo mismo que todavía tengo que encontrar.

Gracias por toda la ayuda.

+0

La forma en que lo hace es correcta. No veo nada mal con su método actual. Cada instancia necesita su propio contador para saber cuándo ha recibido todas las respuestas. Ese es el único método que puedo pensar para resolver su problema. – fearphage

Respuesta

5

Apenas hay otra manera de tener este contador. Otra opción sería usar un objeto {} y agregar una clave para cada solicitud y eliminarla si se completa. De esta forma sabría de inmediato qué ha regresado. Pero la solución permanece igual.

Puede cambiar el código un poco. Si es como en su ejemplo que solo necesita llamar a otra función dentro de CommonCallback (lo llamé otherFunction) entonces no necesita el CommonCallback. Para guardar el contexto, ya usaste cierres. En lugar de

foo.bar.sendRequest(new RequestObject, function(resp) { 
      me.commonCallback(resp); 
      }); 

que podría hacerlo de esta manera

foo.bar.sendRequest(new RequestObject, function(resp) { 
      --me.expectedCallbacks || me.otherFunction(resp); 
      }); 
13

A menos que esté dispuesto a serializar el AJAX, no hay otra manera que se me ocurra de hacer lo que está proponiendo. Dicho esto, creo que lo que tienes es bastante bueno, pero es posible que quieras limpiar un poco la estructura para no ensuciar el objeto que estás creando con los datos de inicialización.

Aquí es una función que te pueden ayudar:

function gate(fn, number_of_calls_before_opening) { 
    return function() { 
     arguments.callee._call_count = (arguments.callee._call_count || 0) + 1; 
     if (arguments.callee._call_count >= number_of_calls_before_opening) 
      fn.apply(null, arguments); 
    }; 
} 

Esta función es lo que se conoce como una función de orden superior - una función que toma funciones como argumentos. Esta función particular devuelve una función que llama a la función pasada cuando se ha llamado number_of_calls_before_opening veces. Por ejemplo:

var f = gate(function(arg) { alert(arg); }, 2); 
f('hello'); 
f('world'); // An alert will popup for this call. 

usted podría hacer uso de esto como su método de devolución de llamada:

foo.bar = function() { 
    var callback = gate(this.method, 2); 
    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

La segunda devolución de llamada, lo que es method se asegurará de que se llama. Pero esto lleva a otro problema: la función gate llama a la función pasada sin ningún contexto, lo que significa que this hará referencia al objeto global, no al objeto que está construyendo. Hay varias formas de evitar esto: puede cerrar this aliasing a me o self. O puede crear otra función de orden superior que hace precisamente eso.

Esto es lo que el primer caso sería el resultado:

foo.bar = function() { 
    var me = this;   
    var callback = gate(function(a,b,c) { me.method(a,b,c); }, 2); 

    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

En este último caso, la otra función de orden superior sería algo así como lo siguiente:

function bind_context(context, fn) { 
    return function() { 
     return fn.apply(context, arguments); 
    }; 
} 

Esta función devuelve una función que llama a la función pasada en el contexto pasado. Un ejemplo de ello sería la siguiente:

var obj = {}; 
var func = function(name) { this.name = name; }; 
var method = bind_context(obj, func); 
method('Your Name!'); 
alert(obj.name); // Your Name! 

Para ponerlo en perspectiva, el código se vería de la siguiente manera:

foo.bar = function() { 
    var callback = gate(bind_context(this, this.method), 2); 

    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

En cualquier caso, una vez que haya tomado estas refactorizaciones tendrá borró el objeto que está construyendo de todos sus miembros que solo son necesarios para la inicialización.

8

puedo añadir que Underscore.js has a nice little helper for this:

crea una versión de la función que sólo se ejecutará después de la primera ser llamado contar veces. Útil para agrupar respuestas asincrónicas, donde desea asegurarse de que todas las llamadas asíncronas hayan finalizado, antes de continuar.

_.after(count, function) 

El código para _after (como de la versión 1.5.0):

_.after = function(times, func) { 
    return function() { 
    if (--times < 1) { 
     return func.apply(this, arguments); 
    } 
    }; 
}; 

The license info (como de la versión 1.5.0)

+1

_.after (count, _.bind (fun, this);) – tribalvibes

2

Eso es algunas cosas buenas Sr. Kyle.

Para ponerlo un poco más simple, usualmente uso una función de Inicio y una función Listo.
-La Start función toma una lista de funciones que se ejecutarán.
-La función Done se llama por las devoluciones de llamada de sus funciones que pasó al método de inicio.
-Addicionalmente, puede pasar una función o lista de funciones al método hecho que se ejecutará cuando se complete la última devolución de llamada.

Las declaraciones tienen este aspecto.

var PendingRequests = 0; 
function Start(Requests) { 
    PendingRequests = Requests.length; 
    for (var i = 0; i < Requests.length; i++) 
     Requests[i](); 
}; 
//Called when async responses complete. 
function Done(CompletedEvents) { 
PendingRequests--; 
    if (PendingRequests == 0) { 
     for (var i = 0; i < CompletedEvents.length; i++) 
      CompletedEvents[i](); 
    } 
} 

Aquí hay un ejemplo simple usando la API de google api.

//Variables 
var originAddress = "*Some address/zip code here*"; //Location A 
var formattedAddress; //Formatted address of Location B 
var distance; //Distance between A and B 
var location; //Location B 

//This is the start function above. Passing an array of two functions defined below. 
Start(new Array(GetPlaceDetails, GetDistances)); 


//This function makes a request to get detailed information on a place. 
//Then callsback with the **GetPlaceDetailsComplete** function 
function GetPlaceDetails() { 
    var request = { 
     reference: location.reference //Google maps reference id 
    }; 
    var PlacesService = new google.maps.places.PlacesService(Map); 
    PlacesService.getDetails(request, GetPlaceDetailsComplete); 
} 

function GetPlaceDetailsComplete(place, status) { 
    if (status == google.maps.places.PlacesServiceStatus.OK) { 
     formattedAddress = place.formatted_address; 
     Done(new Array(PrintDetails)); 
    } 
} 


function GetDistances() { 
    distService = new google.maps.DistanceMatrixService(); 
    distService.getDistanceMatrix(
    { 
     origins: originAddress, 
     destinations: [location.geometry.location], //Location contains lat and lng 
     travelMode: google.maps.TravelMode.DRIVING, 
     unitSystem: google.maps.UnitSystem.IMPERIAL, 
     avoidHighways: false, 
     avoidTolls: false 
    }, GetDistancesComplete); 
} 
function GetDistancesComplete(results, status) { 
    if (status == google.maps.DistanceMatrixStatus.OK) { 
     distance = results[0].distance.text; 
     Done(new Array(PrintDetails)); 
    } 
} 

function PrintDetails() { 
    alert(*Whatever you feel like printing.*); 
} 

Así, en pocas palabras, lo que estamos haciendo aquí es
pase de largo una serie de funciones para la función de inicio
-La inicio función llama a las funciones de la matriz y se establece la número de PendingRequests
-En las devoluciones de llamada para nuestras solicitudes pendientes, llamamos a la función Hecho -La Hecho fu nción toma una matriz de funciones
-La Hecho función disminuye el contador PendingRequests
-Si su son no más solicitudes pendientes, que llamar a las funciones pasan a la función Hecho

Eso es un simple, pero practicle ejemplo de sincronización de llamadas web. Traté de usar un ejemplo de algo que es ampliamente utilizado, así que fui con la API de Google Maps. Espero que alguien encuentre esto útil.

0

Otra forma sería tener un punto de sincronización gracias a un temporizador. No es bonito, pero tiene la ventaja de no tener que agregar la llamada a la siguiente función dentro de la devolución de llamada.

Aquí la función execute_jobs es el punto de entrada. toma una lista de datos para ejecutar simultáneamente. Primero establece el número de trabajos para esperar al tamaño del list. Luego establece un temporizador para probar la condición final (el número que cae a 0). Y finalmente envía un trabajo para cada dato. Cada trabajo reduce la cantidad de trabajos esperados en uno.

Se vería como algo así:

var g_numJobs = 0; 

function async_task(data) { 
    // 
    // ... execute the task on the data ... 
    // 

    // Decrease the number of jobs left to execute. 
    --g_numJobs; 
} 

function execute_jobs(list) { 
    // Set the number of jobs we want to wait for. 
    g_numJobs = list.length; 

    // Set the timer (test every 50ms). 
    var timer = setInterval(function() { 
     if(g_numJobs == 0) { 
      clearInterval(timer); 
      do_next_action(); 
     } 
    }, 50); 

    // Send the jobs. 
    for(var i = 0; i < list.length; ++i) { 
     async_task(list[i])); 
    } 
} 

Para mejorar este código se puede hacer un Job y JobList clases. El Job ejecutará una devolución de llamada y disminuirá el número de trabajos pendientes, mientras que el JobList agregará el temporizador y llamará a la devolución de llamada a la siguiente acción una vez que los trabajos hayan finalizado.

-1

Compartí la misma frustración. Mientras encadenaba más llamadas asincrónicas, se convirtió en un infierno de devolución de llamada. Entonces, se me ocurrió mi propia solución. Estoy seguro de que hay soluciones similares, pero quería crear algo muy simple y fácil de usar. Asynq es un script que escribí para encadenar tareas asincrónicas. Así que se ejecute después de f2 f1, que puede hacer:

asynq.run (f1, f2)

Usted puede encadenar tantas funciones como desee. También puede especificar parámetros o ejecutar una serie de tareas en elementos de una matriz. Espero que esta biblioteca pueda resolver sus problemas o problemas similares que están teniendo otros.

Cuestiones relacionadas