Et dypdykk i JavaScripts Async Iterator-hjelper 'scan', som utforsker dens funksjonalitet, bruksområder og fordeler for asynkron akkumulativ prosessering.
JavaScript Async Iterator-hjelper: Scan – asynkron akkumulativ prosessering
Asynkron programmering er en hjørnestein i moderne JavaScript-utvikling, spesielt når man håndterer I/O-bundne operasjoner, som nettverksforespørsler eller filsysteminteraksjoner. Asynkrone iteratorer, introdusert i ES2018, gir en kraftig mekanisme for å håndtere strømmer av asynkrone data. `scan`-hjelperen, som ofte finnes i biblioteker som RxJS og i økende grad er tilgjengelig som et frittstående verktøy, åpner for enda mer potensial for å prosessere disse asynkrone datastrømmene.
Forstå asynkrone iteratorer
Før vi dykker inn i `scan`, la oss oppsummere hva asynkrone iteratorer er. En asynkron iterator er et objekt som følger protokollen for asynkrone iteratorer. Denne protokollen definerer en `next()`-metode som returnerer et promise som resolver til et objekt med to egenskaper: `value` (den neste verdien i sekvensen) og `done` (en boolsk verdi som indikerer om iteratoren er ferdig). Asynkrone iteratorer er spesielt nyttige når man jobber med data som kommer over tid, eller data som krever asynkrone operasjoner for å hentes.
Her er et grunnleggende 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();
Introduksjon til `scan`-hjelperen
`scan`-hjelperen (også kjent som `accumulate` eller `reduce`) transformerer en asynkron iterator ved å anvende en akkumulatorfunksjon på hver verdi og sende ut det akkumulerte resultatet. Dette er analogt med `reduce`-metoden på arrays, men opererer asynkront og på iteratorer.
I hovedsak tar `scan` en asynkron iterator, en akkumulatorfunksjon og en valgfri startverdi. For hver verdi som sendes ut av kilde-iteratoren, kalles akkumulatorfunksjonen med den forrige akkumulerte verdien (eller startverdien hvis det er første iterasjon) og den nåværende verdien fra iteratoren. Resultatet av akkumulatorfunksjonen blir den neste akkumulerte verdien, som deretter sendes ut av den resulterende asynkrone iteratoren.
Syntaks og parametere
Den generelle syntaksen for å bruke `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 iteratoren som skal transformeres.
- `accumulator`: En funksjon som tar to argumenter: den forrige akkumulerte verdien og den nåværende verdien fra iteratoren. Den skal returnere den nye akkumulerte verdien.
- `initialValue` (valgfri): Startverdien for akkumulatoren. Hvis den ikke er gitt, vil den første verdien fra kilde-iteratoren bli brukt som startverdi, og akkumulatorfunksjonen vil bli kalt fra og med den andre verdien.
Bruksområder og eksempler
`scan`-hjelperen er utrolig allsidig og kan brukes i en rekke scenarioer som involverer asynkrone datastrømmer. Her er noen eksempler:
1. Beregne en løpende sum
Tenk deg at du har en asynkron iterator som sender ut transaksjonsbeløp. Du kan bruke `scan` til å beregne en løpende sum av disse transaksjonene.
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 eksempelet legger `accumulator`-funksjonen simpelthen det nåværende transaksjonsbeløpet til den forrige summen. `initialValue` på 0 sikrer at den løpende summen starter på null.
2. Akkumulere data i en array
Du kan bruke `scan` til å akkumulere data fra en asynkron iterator inn i en array. Dette kan være nyttig for å samle data over tid og prosessere dem i batcher.
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 bruker `accumulator`-funksjonen spread-operatoren (`...`) for å lage en ny array som inneholder alle de tidligere elementene og den nåværende verdien. `initialValue` er en tom array.
3. Implementere en rate limiter (hastighetsbegrenser)
Et mer komplekst bruksområde er å implementere en hastighetsbegrenser. Du kan bruke `scan` til å spore antall forespørsler som er gjort innenfor et visst tidsvindu og forsinke påfølgende forespørsler hvis grensen 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 eksempelet bruker `scan` internt (i `rateLimitedRequests`-funksjonen) for å vedlikeholde en kø av tidsstempler for forespørsler. Den sjekker om antall forespørsler innenfor tidsvinduet overskrider den tillatte grensen. Hvis den gjør det, beregner den nødvendig forsinkelse og pauser før den yielder forespørselen.
4. Bygge en sanntids dataaggregator (globalt eksempel)
Se for deg en global finansiell applikasjon som trenger å aggregere sanntids aksjekurser fra forskjellige børser. En asynkron iterator kan strømme prisoppdateringer fra børser som New York Stock Exchange (NYSE), London Stock Exchange (LSE) og Tokyo Stock Exchange (TSE). `scan` kan brukes til å vedlikeholde en løpende gjennomsnittlig eller høy/lav pris for en bestemt aksje på tvers av 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 eksempelet beregner `accumulator`-funksjonen den løpende summen av priser og antall mottatte oppdateringer. Den endelige gjennomsnittsprisen beregnes deretter fra disse akkumulerte verdiene. Dette gir en sanntidsvisning av aksjekursen på tvers av forskjellige globale markeder.
5. Analysere global nettstedstrafikk
Tenk deg en global webanalyseplattform som mottar strømmer av besøksdata fra servere over hele verden. Hvert datapunkt representerer en bruker som besøker nettstedet. Ved hjelp av `scan` kan vi analysere trenden for sidevisninger per land i sanntid. La oss si at dataene ser slik ut: `{ 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 oppdaterer `accumulator`-funksjonen en teller for hvert land. Utdataene vil vise de akkumulerende sidevisningstallene for hvert land etter hvert som nye besøksdata kommer inn.
Fordeler med å bruke `scan`
`scan`-hjelperen tilbyr flere fordeler når man jobber med asynkrone datastrømmer:
- Deklarativ stil: `scan` lar deg uttrykke akkumulativ prosesseringslogikk på en deklarativ og konsis måte, noe som forbedrer kodens lesbarhet og vedlikeholdbarhet.
- Asynkron håndtering: Den håndterer sømløst asynkrone operasjoner innenfor akkumulatorfunksjonen, noe som gjør den egnet for komplekse scenarioer som involverer I/O-bundne oppgaver.
- Sanntidsprosessering: `scan` muliggjør sanntidsprosessering av datastrømmer, slik at du kan reagere på endringer etter hvert som de skjer.
- Komponerbarhet: Den kan enkelt komponeres med andre asynkrone iterator-hjelpere for å lage komplekse databehandlingspipelines.
Implementere `scan` (hvis den ikke er tilgjengelig)
Selv om noen biblioteker tilbyr en innebygd `scan`-hjelper, kan du enkelt implementere din egen om nødvendig. Her er en enkel 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 implementeringen itererer over kilde-iteratoren og anvender akkumulatorfunksjonen på hver verdi, og yielder det akkumulerte resultatet. Den håndterer tilfellet der ingen `initialValue` er gitt ved å bruke den første verdien fra kilde-iteratoren som startverdi.
Sammenligning med `reduce`
Det er viktig å skille `scan` fra `reduce`. Selv om begge opererer på iteratorer og bruker en akkumulatorfunksjon, skiller de seg i oppførsel og utdata.
- `scan` sender ut den akkumulerte verdien for hver iterasjon, og gir en løpende historikk over akkumuleringen.
- `reduce` sender kun ut den endelige akkumulerte verdien etter å ha prosessert alle elementene i iteratoren.
Derfor er `scan` egnet for scenarioer der du trenger å spore de mellomliggende tilstandene av akkumuleringen, mens `reduce` er passende når du kun trenger det endelige resultatet.
Feilhåndtering
Når du jobber med asynkrone iteratorer og `scan`, er det avgjørende å håndtere feil på en elegant måte. Feil kan oppstå under iterasjonsprosessen eller i akkumulatorfunksjonen. Du kan bruke `try...catch`-blokker for å fange opp og håndtere disse feilene.
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 eksempelet fanger `try...catch`-blokken feilen som kastes av `generatePotentiallyFailingData`-iteratoren. Du kan deretter håndtere feilen på en passende måte, som å logge den eller prøve operasjonen på nytt.
Konklusjon
`scan`-hjelperen er et kraftig verktøy for å utføre asynkron akkumulativ prosessering på JavaScripts asynkrone iteratorer. Den lar deg uttrykke komplekse datatransformasjoner på en deklarativ og konsis måte, håndtere asynkrone operasjoner elegant, og prosessere datastrømmer i sanntid. Ved å forstå dens funksjonalitet og bruksområder kan du utnytte `scan` til å bygge mer robuste og effektive asynkrone applikasjoner. Enten du beregner løpende summer, akkumulerer data i arrays, implementerer hastighetsbegrensere eller bygger sanntids dataaggregatorer, kan `scan` forenkle koden din og forbedre den generelle ytelsen. Husk å vurdere feilhåndtering og velge `scan` over `reduce` når du trenger tilgang til mellomliggende akkumulerte verdier under prosesseringen av dine asynkrone datastrømmer. Å utforske biblioteker som RxJS kan ytterligere forbedre din forståelse og praktiske anvendelse av `scan` innenfor reaktive programmeringsparadigmer.