2009-05-13 12 views
20

Ho un oggetto Javascript che richiede 2 chiamate a un server esterno per creare il suo contenuto e fare qualcosa di significativo. L'oggetto è costruito in modo tale che l'istanziazione di una sua istanza farà automaticamente queste 2 chiamate. Le 2 chiamate condividono una funzione di callback comune che opera sui dati restituiti e quindi chiama un altro metodo. Il problema è che il prossimo metodo non dovrebbe essere chiamato fino al ritorno di entrambi i metodi. Ecco il codice come ho implementato attualmente:Javascript - sincronizzazione dopo chiamate asincrone

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

Come potete vedere, sto costringendo l'oggetto di continuare dopo le chiamate sono tornati con un semplice contatore per convalidare entrambi hanno restituito. Funziona ma sembra un'implementazione davvero scarsa. Ho lavorato con Javascript solo per alcune settimane e mi chiedo se esiste un metodo migliore per fare la stessa cosa su cui devo ancora imbattermi.

Grazie per qualsiasi aiuto.

+0

Il modo in cui lo stai facendo è corretto. Non vedo nulla di sbagliato con il tuo metodo attuale. Ogni istanza ha bisogno del proprio contatore per sapere quando ha ricevuto tutte le risposte. Questo è l'unico metodo che posso pensare per risolvere il tuo problema. – fearphage

risposta

5

C'è appena un altro modo di avere questo contatore. Un'altra opzione sarebbe quella di usare un oggetto {} e aggiungere una chiave per ogni richiesta e rimuoverla se finita. In questo modo sapresti immediatamente quale è stato restituito. Ma la soluzione rimane la stessa.

È possibile modificare leggermente il codice. Se è come nel tuo esempio che hai solo bisogno di chiamare un'altra funzione all'interno di commonCallback (l'ho chiamata otherFunction) che non hai bisogno di commonCallback. Per salvare il contesto hai già utilizzato le chiusure. Invece di

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

si potrebbe fare in questo modo

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

A meno che non si sia disposti a serializzare l'AJAX, non c'è altro modo in cui posso pensare di fare ciò che stai proponendo. Detto questo, penso che quello che hai è abbastanza buono, ma potresti voler pulire un po 'la struttura per non sporcare l'oggetto che stai creando con i dati di inizializzazione.

Ecco una funzione che potrebbe aiutare:

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

Questa funzione è ciò che è noto come una funzione di ordine superiore - una funzione che prende funzioni come argomenti. Questa particolare funzione restituisce una funzione che chiama la funzione passata quando è stata chiamata number_of_calls_before_opening volte. Per esempio:

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

si potrebbe fare uso di questo come metodo di callback:

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

La seconda callback, qualunque essa sia farà in modo che method si chiama. Ma questo porta a un altro problema: la funzione gate chiama la funzione passata senza alcun contesto, ovvero this si riferirà all'oggetto globale, non all'oggetto che si sta costruendo. Esistono diversi modi per aggirare questo problema: puoi chiudere lo this aliasandolo a me o self. Oppure puoi creare un'altra funzione di ordine superiore che fa proprio questo.

Ecco ciò che il primo caso sarà simile:

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

In quest'ultimo caso, l'altra funzione di ordine superiore sarebbe qualcosa di simile al seguente:

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

Questa funzione restituisce una funzione che chiama la funzione passata nel contesto passato. Un esempio di esso sarebbe la seguente:

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

Per dirla in prospettiva, il codice sarà il seguente:

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

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

In ogni caso, una volta che hai fatto queste refactoring si avrà chiarito l'oggetto in costruzione di tutti i suoi membri che sono necessari solo per l'inizializzazione.

8

posso aggiungere che Underscore.js has a nice little helper for this:

crea una versione della funzione che verrà eseguito solo dopo la prima essere stato chiamato conta volte. Utile per raggruppare risposte asincrone, in cui si desidera essere sicuri che tutte le chiamate asincrone siano terminate, prima di procedere.

_.after(count, function) 

Il codice per _after (as-versione 1.5.0):

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

The license info (as-versione 1.5.0)

+1

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

2

Ecco alcune cose buone Mr. Kyle.

Per dirla in modo un po 'più semplice, di solito uso una funzione Start e una Fine.
-La funzione Start accetta un elenco di funzioni che verranno eseguite.
-The La funzione terminata viene richiamata dai richiami delle funzioni che è passato al metodo di avvio.
-Inoltre, è possibile passare una funzione o un elenco di funzioni al metodo done che verrà eseguito al termine dell'ultima callback.

Le dichiarazioni sono simili a questa.

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

Ecco un semplice esempio utilizzando l'API di google maps.

//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.*); 
} 

Quindi, in poche parole, quello che stiamo facendo qui è
-Passing una serie di funzioni per la funzione di avvio
-La funzione Inizio chiama le funzioni nella matrice e imposta il numero di PendingRequests
-In i richiami per le nostre richieste in sospeso, chiamiamo la funzione Fatto -Il Fatto fu nction prende una serie di funzioni
-La Fatto funzione decrementa il contatore PendingRequests
-Se loro sono richieste non più sospeso, chiamiamo le funzioni passati alla funzione Fatto

Questo è un semplice, ma practicle esempio di chiamate Web sincronizzate. Ho provato a utilizzare un esempio di qualcosa che è ampiamente utilizzato, quindi sono andato con l'API di Google Maps. Spero che qualcuno lo trovi utile.

0

Un altro modo sarebbe avere un punto di sincronizzazione grazie a un timer. Non è bello, ma ha il vantaggio di non dover aggiungere la chiamata alla funzione successiva all'interno del callback.

Qui la funzione execute_jobs è il punto di ingresso. prende una lista di dati da eseguire simultaneamente. Prima imposta il numero di lavori in attesa della dimensione di list. Quindi imposta un timer per testare la condizione di fine (il numero scende a 0). E infine invia un lavoro per ogni dato. Ogni lavoro diminuisce di uno il numero di lavori attesi.

E sarebbe simile a qualcosa di simile:

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

di migliorare questo codice è possibile fare un Job e JobList classi. Lo Job eseguiva una richiamata e diminuiva il numero di lavori in sospeso, mentre lo JobList aggregava il timer e richiamava la richiamata all'azione successiva una volta che i lavori sono terminati.

-1

Ho condiviso la stessa frustrazione. Come ho incatenato più chiamate asincrone, è diventato un inferno di callback. Quindi, ho trovato la mia soluzione. Sono sicuro che ci siano soluzioni simili là fuori, ma volevo creare qualcosa di molto semplice e facile da usare. Asynq è uno script che ho scritto per concatenare attività asincrone. Quindi, per eseguire f2 f1 dopo, si può fare:

asynq.run (f1, f2)

È possibile concatenare come molte funzioni come si desidera. È inoltre possibile specificare parametri o eseguire una serie di attività sugli elementi di una matrice. Spero che questa libreria possa risolvere i tuoi problemi o problemi simili che altri stanno avendo.

Problemi correlati