javascript - Håndtere samtidige async-oppdateringer til lokal tilstand
Jeg har en serie asynkrone kall som leser fra en lokal tilstand S, utfører noen beregninger basert på dens nåværende verdi, og returnerer en ny, oppdatert verdi av den lokale tilstanden S'
Alt dette skjer under kjøring, så jeg har veldig liten kontroll over rekkefølgen på disse operasjonene. Dette er en forenklet versjon av det jeg har.
type State = {
state: number
}
let localState: State = {
state: 1000
}
const promiseTimeout = (time: number, value: number) => () => new Promise(
(resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
);
const post: (n: number, currentState: State) => Promise<void> = (n, c) => promiseTimeout(n, c.state)()
.then(res => {
localState.state = res
console.log(localState)
})
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
// when both promises resolve, the final value of localState will be 4000 instead of 5000
Lekerom lenke
Denne modellen er tydeligvis ødelagt, siden begge kallene til post vil lese den samme verdien av localState, mens de i stedet burde utføres sekvensielt.
Hvis alle kall allerede var bestemt på kompileringstidspunktet, kunne jeg rett og slett hatt noe som
post(1000, localState)
.then(() => post(3000, localState)) // localState at call time is now 2000
Hvordan ville jeg gått fram for å løse dette?
Svar #1
En tilnærming er å la post koble seg til et løfte i stedet for å jobbe direkte på tilstandsobjektet. Dette løftet kan lagres i tilstandsobjektet selv. Det starter som oppfylt med tilstandsobjektet. post oppdaterer det slik:
const post = (n, state) => {
return state.promise = state.promise
.then(state => {
// ...do stuff here that updates (or replaces) `state`...
return state;
}));
};
Her er et eksempel (i JavaScript, men du kan legge til typeannotasjoner igjen) som bruker asyncAction (det er som din promiseTimeout, men uten å få det til å returnere en funksjon vi kaller umiddelbart; ikke
"use strict";
let localState = {
state: 1000
};
localState.promise = Promise.resolve(localState);
// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time, value) => () => new Promise((resolve) => setTimeout(resolve, time, value + time));
const post = (n, state) => {
return state.promise = state.promise
.then(state => promiseTimeout(n, state.state)().then(newValue => {
state.state = newValue;
console.log(state.state);
return state;
}));
};
console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000Siden hvert kall til post synkront erstatter løftet med et nytt løfte, bygges kjeden av kallene til post.
Her er det i TypeScript (med en liten jukseløsning på ett sted, du kan sikkert forbedre det); lenke til lekeplassen.
type State = {
state: number,
promise: Promise<State>
};
let localState: State = (() => {
const s: Partial<State> = {
state: 1000
};
// There's probably a better way to handle this than type assertions, but...
s.promise = Promise.resolve(s as State);
return s as State;
})();
// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time: number, value: number) => () => new Promise(
(resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
);
const post = (n: number, state: State): Promise<State> => {
return state.promise = state.promise
.then(state => promiseTimeout(n, state.state)().then(newValue => {
state.state = newValue;
console.log(state.state);
return state;
}));
};
console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
Det er verdt å merke seg at i situasjoner som dette, hvor tilstanden kan endres asynkront på denne måten, er det ofte verdt å produsere et nytt tilstandsobjekt når man endrer det i stedet for å modifisere det eksisterende — for eksempel å behandle tilstandsaspektene som uforanderlige.
Svar #2
Dette er et problem som jeg personlig har støtt på mange ganger. Min løsning er å lage en køklasse som har ansvaret for å sørge for at alle Promise utføres med gjensidig utelukkelse. Jeg kaller den PromiseQueue:
class PromiseQueue {
constructor() {
this._queue = new Array(); // Or an LinkedList for better performance
this._usingQueue = false;
}
/**
* Adds an element to the queue and runs the queue. It resolves when the promise has been executed and resolved.
*
* @param {Promise<any>} promise
*/
add(promise) {
const self = this;
return new Promise((resolve, reject) => {
const promiseData = {
promise,
resolve,
reject,
};
self._queue.push(promiseData);
self._runQueue();
});
}
async _runQueue() {
if (!this._usingQueue && this._queue.length > 0) {
this._usingQueue = true;
const nextPromiseData = this._queue.shift();
const { promise, resolve, reject } = nextPromiseData;
try {
const result = await promise();
resolve(result);
} catch (e) {
reject(e);
}
this._usingQueue = false;
this._runQueue();
}
}
}
Da ville du brukt det slik (ikke testet):
const myPromiseQueue = new PromiseQueue();
// This way you are making sure that the second post
// will be executed when the first one has finished
myPromiseQueue.add(async() => await post(1000, localState));
myPromiseQueue.add(async() => await post(3000, localState));
