Et dybdegĂĄende kig pĂĄ JavaScript Async Iterator Helper 'scan', der udforsker dens funktionalitet, brugsscenarier og fordele for asynkron akkumulativ behandling.
JavaScript Async Iterator Helper: Scan - Asynkron Akkumulativ Behandling
Asynkron programmering er en hjørnesten i moderne JavaScript-udvikling, især når man håndterer I/O-bundne operationer, såsom netværksanmodninger eller filsysteminteraktioner. Asynkrone iteratorer, introduceret i ES2018, giver en kraftfuld mekanisme til håndtering af strømme af asynkrone data. `scan`-helperen, som ofte findes i biblioteker som RxJS og i stigende grad er tilgængelig som et selvstændigt værktøj, åbner for endnu mere potentiale for behandling af disse asynkrone datastrømme.
ForstĂĄelse af Asynkrone Iteratorer
Før vi dykker ned i `scan`, lad os opsummere, hvad asynkrone iteratorer er. En asynkron iterator er et objekt, der overholder den asynkrone iterator-protokol. Denne protokol definerer en `next()`-metode, der returnerer et promise, som resolver til et objekt med to egenskaber: `value` (den næste værdi i sekvensen) og `done` (en boolean, der angiver, om iteratoren er færdig). Asynkrone iteratorer er særligt nyttige, når man arbejder med data, der ankommer over tid, eller data, der kræver asynkrone operationer for at blive hentet.
Her er et grundlæggende eksempel på en asynkron iterator:
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
async function main() {
const iterator = generateNumbers();
let result = await iterator.next();
console.log(result); // { value: 1, done: false }
result = await iterator.next();
console.log(result); // { value: 2, done: false }
result = await iterator.next();
console.log(result); // { value: 3, done: false }
result = await iterator.next();
console.log(result); // { value: undefined, done: true }
}
main();
Introduktion til `scan` Helper
`scan`-helperen (også kendt som `accumulate` eller `reduce`) transformerer en asynkron iterator ved at anvende en akkumulatorfunktion på hver værdi og udsende det akkumulerede resultat. Dette svarer til `reduce`-metoden på arrays, men fungerer asynkront og på iteratorer.
I bund og grund tager `scan` en asynkron iterator, en akkumulatorfunktion og en valgfri startværdi. For hver værdi, der udsendes af kilde-iteratoren, kaldes akkumulatorfunktionen med den forrige akkumulerede værdi (eller startværdien, hvis det er den første iteration) og den aktuelle værdi fra iteratoren. Resultatet af akkumulatorfunktionen bliver den næste akkumulerede værdi, som derefter udsendes af den resulterende asynkrone iterator.
Syntaks og Parametre
Den generelle syntaks for brug af `scan` er som følger:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator`: Den asynkrone iterator, der skal transformeres.
- `accumulator`: En funktion, der tager to argumenter: den forrige akkumulerede værdi og den aktuelle værdi fra iteratoren. Den skal returnere den nye akkumulerede værdi.
- `initialValue` (valgfri): Den indledende værdi for akkumulatoren. Hvis den ikke angives, vil den første værdi fra kilde-iteratoren blive brugt som startværdi, og akkumulatorfunktionen vil blive kaldt fra og med den anden værdi.
Brugsscenarier og Eksempler
`scan`-helperen er utroligt alsidig og kan bruges i en lang række scenarier, der involverer asynkrone datastrømme. Her er et par eksempler:
1. Beregning af en Løbende Total
Forestil dig, at du har en asynkron iterator, der udsender transaktionsbeløb. Du kan bruge `scan` til at beregne en løbende total af disse transaktioner.
async function* generateTransactions() {
yield 10;
yield 20;
yield 30;
}
async function main() {
const transactions = generateTransactions();
const runningTotals = scan(transactions, (acc, value) => acc + value, 0);
for await (const total of runningTotals) {
console.log(total); // Output: 10, 30, 60
}
}
main();
I dette eksempel lægger `accumulator`-funktionen simpelthen det aktuelle transaktionsbeløb til den forrige total. `initialValue` på 0 sikrer, at den løbende total starter fra nul.
2. Akkumulering af Data i et Array
Du kan bruge `scan` til at akkumulere data fra en asynkron iterator i et array. Dette kan være nyttigt til at indsamle data over tid og behandle dem i batches.
async function* fetchData() {
yield { id: 1, name: 'Alice' };
yield { id: 2, name: 'Bob' };
yield { id: 3, name: 'Charlie' };
}
async function main() {
const dataStream = fetchData();
const accumulatedData = scan(dataStream, (acc, value) => [...acc, value], []);
for await (const data of accumulatedData) {
console.log(data); // Output: [{id: 1, name: 'Alice'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
}
}
main();
Her bruger `accumulator`-funktionen spread-operatoren (`...`) til at oprette et nyt array, der indeholder alle de tidligere elementer og den aktuelle værdi. `initialValue` er et tomt array.
3. Implementering af en Rate Limiter
Et mere komplekst brugsscenarie er implementering af en rate limiter. Du kan bruge `scan` til at spore antallet af anmodninger inden for et bestemt tidsvindue og forsinke efterfølgende anmodninger, hvis hastighedsgrænsen overskrides.
async function* generateRequests() {
// Simulate incoming requests
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 200));
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
yield Date.now();
}
async function main() {
const requests = generateRequests();
const rateLimitWindow = 1000; // 1 second
const maxRequestsPerWindow = 2;
async function* rateLimitedRequests(source, window, maxRequests) {
let queue = [];
for await (const requestTime of source) {
queue.push(requestTime);
queue = queue.filter(t => requestTime - t < window);
if (queue.length > maxRequests) {
const earliestRequest = queue[0];
const delay = window - (requestTime - earliestRequest);
console.log(`Rate limit exceeded. Delaying for ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
yield requestTime;
}
}
const limited = rateLimitedRequests(requests, rateLimitWindow, maxRequestsPerWindow);
for await (const requestTime of limited) {
console.log(`Request processed at ${requestTime}`);
}
}
main();
Dette eksempel bruger `scan` internt (i `rateLimitedRequests`-funktionen) til at vedligeholde en kø af anmodningstidsstempler. Det tjekker, om antallet af anmodninger inden for rate limit-vinduet overstiger det maksimalt tilladte. Hvis det gør, beregner den den nødvendige forsinkelse og pauser, før den yielder anmodningen.
4. Opbygning af en Realtids Dataaggregator (Globalt Eksempel)
Overvej en global finansiel applikation, der skal aggregere aktiekurser i realtid fra forskellige børser. En asynkron iterator kunne streame kursopdateringer fra børser som New York Stock Exchange (NYSE), London Stock Exchange (LSE) og Tokyo Stock Exchange (TSE). `scan` kan bruges til at vedligeholde et løbende gennemsnit eller en høj/lav pris for en bestemt aktie på tværs af alle børser.
// Simulate streaming stock prices from different exchanges
async function* generateStockPrices() {
yield { exchange: 'NYSE', symbol: 'AAPL', price: 170.50 };
yield { exchange: 'LSE', symbol: 'AAPL', price: 170.75 };
await new Promise(resolve => setTimeout(resolve, 50));
yield { exchange: 'TSE', symbol: 'AAPL', price: 170.60 };
}
async function main() {
const stockPrices = generateStockPrices();
// Use scan to calculate a running average price
const runningAverages = scan(
stockPrices,
(acc, priceUpdate) => {
const { total, count } = acc;
return { total: total + priceUpdate.price, count: count + 1 };
},
{ total: 0, count: 0 }
);
for await (const averageData of runningAverages) {
const averagePrice = averageData.total / averageData.count;
console.log(`Running average price: ${averagePrice.toFixed(2)}`);
}
}
main();
I dette eksempel beregner `accumulator`-funktionen den løbende total af priser og antallet af modtagne opdateringer. Den endelige gennemsnitspris beregnes derefter ud fra disse akkumulerede værdier. Dette giver et realtidsbillede af aktiekursen på tværs af forskellige globale markeder.
5. Analyse af Hjemmesidetrafik Globalt
Forestil dig en global webanalyseplatform, der modtager strømme af data om hjemmesidebesøg fra servere placeret rundt om i verden. Hvert datapunkt repræsenterer en bruger, der besøger hjemmesiden. Ved hjælp af `scan` kan vi analysere tendensen for sidevisninger pr. land i realtid. Lad os sige, at dataene ser sådan ud: `{ country: "US", page: "homepage", timestamp: 1678886400 }`.
async function* generateWebsiteVisits() {
yield { country: 'US', page: 'homepage', timestamp: Date.now() };
yield { country: 'CA', page: 'product', timestamp: Date.now() };
yield { country: 'UK', page: 'blog', timestamp: Date.now() };
yield { country: 'US', page: 'product', timestamp: Date.now() };
}
async function main() {
const visitStream = generateWebsiteVisits();
const pageViewCounts = scan(
visitStream,
(acc, visit) => {
const { country } = visit;
const newAcc = { ...acc };
newAcc[country] = (newAcc[country] || 0) + 1;
return newAcc;
},
{}
);
for await (const counts of pageViewCounts) {
console.log('Page view counts by country:', counts);
}
}
main();
Her opdaterer `accumulator`-funktionen en tæller for hvert land. Outputtet ville vise de akkumulerende sidevisningstællinger for hvert land, efterhånden som nye besøgsdata ankommer.
Fordele ved at Bruge `scan`
`scan`-helperen tilbyder flere fordele, når man arbejder med asynkrone datastrømme:
- Deklarativ Stil: `scan` giver dig mulighed for at udtrykke akkumulativ behandlingslogik på en deklarativ og præcis måde, hvilket forbedrer kodens læsbarhed og vedligeholdelighed.
- Asynkron Håndtering: Den håndterer sømløst asynkrone operationer inden i akkumulatorfunktionen, hvilket gør den velegnet til komplekse scenarier, der involverer I/O-bundne opgaver.
- Realtidsbehandling: `scan` muliggør behandling af datastrømme i realtid, så du kan reagere på ændringer, efterhånden som de sker.
- Komponerbarhed: Den kan let kombineres med andre asynkrone iterator-helpers for at skabe komplekse databehandlings-pipelines.
Implementering af `scan` (Hvis den ikke er tilgængelig)
Mens nogle biblioteker tilbyder en indbygget `scan`-helper, kan du nemt implementere din egen, hvis det er nødvendigt. Her er en simpel implementering:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
let first = true;
for await (const value of sourceIterator) {
if (first && initialValue === undefined) {
accumulatedValue = value;
first = false;
} else {
accumulatedValue = accumulator(accumulatedValue, value);
}
yield accumulatedValue;
}
}
Denne implementering itererer over kilde-iteratoren og anvender akkumulatorfunktionen på hver værdi, og yielder det akkumulerede resultat. Den håndterer det tilfælde, hvor der ikke er angivet nogen `initialValue`, ved at bruge den første værdi fra kilde-iteratoren som startværdi.
Sammenligning med `reduce`
Det er vigtigt at skelne `scan` fra `reduce`. Selvom begge opererer på iteratorer og bruger en akkumulatorfunktion, adskiller de sig i deres adfærd og output.
- `scan` udsender den akkumulerede værdi for hver iteration, hvilket giver en løbende historik over akkumuleringen.
- `reduce` udsender kun den endelige akkumulerede værdi efter at have behandlet alle elementer i iteratoren.
Derfor er `scan` velegnet til scenarier, hvor du har brug for at spore de mellemliggende tilstande af akkumuleringen, mens `reduce` er passende, nĂĄr du kun har brug for det endelige resultat.
FejlhĂĄndtering
Når man arbejder med asynkrone iteratorer og `scan`, er det afgørende at håndtere fejl elegant. Fejl kan opstå under iterationsprocessen eller inde i akkumulatorfunktionen. Du kan bruge `try...catch`-blokke til at fange og håndtere disse fejl.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Something went wrong!');
yield 3;
}
async function main() {
const dataStream = generatePotentiallyFailingData();
try {
const accumulatedData = scan(dataStream, (acc, value) => acc + value, 0);
for await (const data of accumulatedData) {
console.log(data);
}
} catch (error) {
console.error('An error occurred:', error);
}
}
main();
I dette eksempel fanger `try...catch`-blokken den fejl, der kastes af `generatePotentiallyFailingData`-iteratoren. Du kan derefter håndtere fejlen passende, f.eks. ved at logge den eller prøve operationen igen.
Konklusion
`scan`-helperen er et kraftfuldt værktøj til at udføre asynkron akkumulativ behandling på JavaScripts asynkrone iteratorer. Den giver dig mulighed for at udtrykke komplekse datatransformationer på en deklarativ og præcis måde, håndtere asynkrone operationer elegant og behandle datastrømme i realtid. Ved at forstå dens funktionalitet og brugsscenarier kan du udnytte `scan` til at bygge mere robuste og effektive asynkrone applikationer. Uanset om du beregner løbende totaler, akkumulerer data i arrays, implementerer rate limiters eller bygger realtids dataaggregatorer, kan `scan` forenkle din kode og forbedre dens overordnede ydeevne. Husk at overveje fejlhåndtering og vælge `scan` frem for `reduce`, når du har brug for adgang til mellemliggende akkumulerede værdier under behandlingen af dine asynkrone datastrømme. Udforskning af biblioteker som RxJS kan yderligere forbedre din forståelse og praktiske anvendelse af `scan` inden for reaktive programmeringsparadigmer.