Usa async / await per implementare l'esclusione reciproca in JavaScript
Avevo bisogno di eseguire una sequenza di attività asincrone in JavaScript. Sono asincroni perché ho bisogno che non siano bloccanti, ma voglio comunque che l'attività corrente termini prima di iniziare quella successiva. L'ordine dei compiti non ha importanza, l'unica cosa importante è che siano in mutua esclusione tra loro.
Per questo motivo, forse a causa del background C / C ++, stavo per implementare una coda e un sistema di blocchi. Quindi mi fermo a pensare se il problema potesse essere risolto semplicemente usando async / await, e ho finito con questa soluzione:
previousPromise = null;
async function enqueue(task) {
while (previousPromise) {
await previousPromise;
}
previousPromise = executeTask(task);
await previousPromise;
previousPromise = null;
}
In pratica, utilizzo la coda interna dei sottoscrittori della promessa come coda di blocco, in modo da ottenere un'attesa non bloccante. Quando la promessa viene risolta, vengono attivate tutte le attività asincrone † e il primo risultato previousPromiseuguale a null continuerà.
Ecco un esempio di codepen .
C'è qualche problema in questa soluzione? Grazie
Nota a margine 1 : ovviamente non è necessario un vero sistema di chiusura. JavaScript è a thread singolo, quindi non c'è alcun rischio di essere anticipati dopo il tempo e prima
executeTaskdell'assegnazione.Nota a margine 2 : le attività arrivano dal server in modo asincrono, quindi non posso raccoglierle tutte prima dell'esecuzione.
† In realtà chiama la prima richiamata nell'elenco dell'abbonato
Risposte
Questo ciclo:
async function enqueue(task) {
while (previousPromise) {
await previousPromise;
}
si bloccherà (in modo asincrono) per sempre, dopo che la prima volta previousPromiseè stata assegnata a. Un'espressione che è una promessa (ad esempio, il previousPromise) rimarrà sempre una promessa e sarà veritiera. È possibile utilizzare await previousPromiseper estrarre il valore di risoluzione in un'espressione diversa, ma previousPromiserimane una Promessa. Non più di due attività possono essere completate utilizzando quello enqueue.
Un metodo migliore sarebbe riassegnare la promessa ogni volta che enqueueviene chiamata, mentre si concatena alla promessa precedente con .then:
previousPromise = previousPromise.then(() => executeTask(task));
Sarà anche più facile da gestire se previousPromiserimane la stessa forma (cioè di una Promessa), piuttosto che doverlo gestire possibilmente essere null. Inizializzalo su Promise.resolve().
Demo di seguito, in cui un'attività richiede 1 secondo: avvia immediatamente 2 attività, quindi altre 2 attività dopo 1,5 secondi (quindi queste 4 insieme terminano dopo 4 secondi), quindi un'altra dopo 7 secondi (che termina al secondo 8).
const executeTask = () => new Promise(resolve => setTimeout(() => {
console.log('task resolving');
resolve();
}, 1000));
let previousPromise = Promise.resolve();
function enqueue(task) {
previousPromise = previousPromise.then(() => executeTask(task));
}
console.log('start');
enqueue();
enqueue();
setTimeout(() => {
enqueue();
enqueue();
}, 1500);
setTimeout(() => {
enqueue();
}, 7000);
Un'altra cosa da considerare è: cosa vuoi che accada quando uno dei compiti viene rifiutato (se mai è possibile)? Potresti voler aggiungere un .catchalla fine di executeTaskper assicurarti che l'attività successiva possa essere avviata, senza che l'intera catena di Promise si interrompa:
previousPromise = previousPromise
.then(() => executeTask(task))
.catch((err) => { /* log error? Make sure previousPromise always resolves */ });