Syväsukellus JavaScriptin 'scan'-apuohjelmaan, tutkien sen toiminnallisuutta, käyttötapauksia ja etuja asynkronisessa kumulatiivisessa käsittelyssä.
JavaScriptin asynkroninen iteraattoriapuohjelma: Scan - asynkroninen kumulatiivinen käsittely
Asynkroninen ohjelmointi on modernin JavaScript-kehityksen kulmakivi, erityisesti kun käsitellään I/O-sidonnaisia operaatioita, kuten verkkopyyntöjä tai tiedostojärjestelmän vuorovaikutusta. Asynkroniset iteraattorit, jotka esiteltiin ES2018:ssa, tarjoavat tehokkaan mekanismin asynkronisten datavirtojen käsittelyyn. `scan`-apuohjelma, joka löytyy usein RxJS:n kaltaisista kirjastoista ja on yhä useammin saatavilla itsenäisenä apuohjelmana, avaa entistä enemmän potentiaalia näiden asynkronisten datavirtojen käsittelyyn.
Asynkronisten iteraattoreiden ymmärtäminen
Ennen kuin sukellamme `scan`-apuohjelmaan, kerrataan mitä asynkroniset iteraattorit ovat. Asynkroninen iteraattori on objekti, joka noudattaa asynkronisen iteraattorin protokollaa. Tämä protokolla määrittelee `next()`-metodin, joka palauttaa promisen, joka ratkeaa objektiksi, jolla on kaksi ominaisuutta: `value` (sekvenssin seuraava arvo) ja `done` (boolean-arvo, joka ilmaisee, onko iteraattori päättynyt). Asynkroniset iteraattorit ovat erityisen hyödyllisiä, kun työskennellään ajan myötä saapuvan datan kanssa tai datan kanssa, jonka noutaminen vaatii asynkronisia operaatioita.
Tässä on perusesimerkki asynkronisesta iteraattorista:
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();
`scan`-apuohjelman esittely
`scan`-apuohjelma (tunnetaan myös nimillä `accumulate` tai `reduce`) muuntaa asynkronisen iteraattorin soveltamalla kertymäfunktiota jokaiseen arvoon ja lähettämällä kertyneen tuloksen. Tämä on analoginen taulukoiden `reduce`-metodin kanssa, mutta se toimii asynkronisesti ja iteraattoreilla.
Pohjimmiltaan `scan` ottaa asynkronisen iteraattorin, kertymäfunktion ja valinnaisen alkuarvon. Jokaiselle lähdeiteraattorin lähettämälle arvolle kertymäfunktiota kutsutaan edellisellä kertyneellä arvolla (tai alkuarvolla, jos kyseessä on ensimmäinen iteraatio) ja iteraattorin nykyisellä arvolla. Kertymäfunktion tuloksesta tulee seuraava kertyvä arvo, jonka tuloksena oleva asynkroninen iteraattori sitten lähettää.
Syntaksi ja parametrit
`scan`-apuohjelman yleinen syntaksi on seuraava:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator`: Muunnettava asynkroninen iteraattori.
- `accumulator`: Funktio, joka ottaa kaksi argumenttia: edellisen kertyneen arvon ja nykyisen arvon iteraattorista. Sen tulee palauttaa uusi kertyvä arvo.
- `initialValue` (valinnainen): Kertymän alkuarvo. Jos sitä ei anneta, lähdeiteraattorin ensimmäistä arvoa käytetään alkuarvona, ja kertymäfunktiota aletaan kutsua toisesta arvosta lähtien.
Käyttötapaukset ja esimerkit
`scan`-apuohjelma on uskomattoman monipuolinen ja sitä voidaan käyttää monenlaisissa skenaarioissa, jotka liittyvät asynkronisiin datavirtoihin. Tässä on muutama esimerkki:
1. Juoksevan summan laskeminen
Kuvittele, että sinulla on asynkroninen iteraattori, joka lähettää tapahtumien summia. Voit käyttää `scan`-apuohjelmaa laskeaksesi näiden tapahtumien juoksevan summan.
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); // Tuloste: 10, 30, 60
}
}
main();
Tässä esimerkissä `accumulator`-funktio yksinkertaisesti lisää nykyisen tapahtuman summan edelliseen summaan. `initialValue` 0 varmistaa, että juokseva summa alkaa nollasta.
2. Datan kerääminen taulukkoon
Voit käyttää `scan`-apuohjelmaa kerätäksesi dataa asynkronisesta iteraattorista taulukkoon. Tämä voi olla hyödyllistä datan keräämisessä ajan myötä ja sen käsittelyssä erissä.
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); // Tuloste: [{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();
Tässä `accumulator`-funktio käyttää spread-operaattoria (`...`) luodakseen uuden taulukon, joka sisältää kaikki aiemmat elementit ja nykyisen arvon. `initialValue` on tyhjä taulukko.
3. Nopeusrajoittimen toteuttaminen
Monimutkaisempi käyttötapaus on nopeusrajoittimen toteuttaminen. Voit käyttää `scan`-apuohjelmaa seurataksesi tehtyjen pyyntöjen määrää tietyn aikaikkunan sisällä ja viivästyttääksesi myöhempiä pyyntöjä, jos nopeusrajoitus ylittyy.
async function* generateRequests() {
// Simuloidaan saapuvia pyyntöjä
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 sekunti
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(`Nopeusrajoitus ylitetty. Viivytetään ${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(`Pyyntö käsitelty hetkellä ${requestTime}`);
}
}
main();
Tämä esimerkki käyttää `scan`-apuohjelmaa sisäisesti (`rateLimitedRequests`-funktiossa) ylläpitääkseen jonoa pyyntöjen aikaleimoista. Se tarkistaa, ylittääkö pyyntöjen määrä nopeusrajoitusikkunan sisällä sallitun maksimimäärän. Jos näin on, se laskee tarvittavan viiveen ja pysähtyy ennen pyynnön tuottamista.
4. Reaaliaikaisen datan kerääjän rakentaminen (globaali esimerkki)
Kuvittele maailmanlaajuinen rahoitussovellus, jonka on kerättävä reaaliaikaisia osakekursseja eri pörsseistä. Asynkroninen iteraattori voisi suoratoistaa hintapäivityksiä pörsseistä, kuten New Yorkin pörssistä (NYSE), Lontoon pörssistä (LSE) ja Tokion pörssistä (TSE). `scan`-apuohjelmaa voidaan käyttää ylläpitämään juoksevaa keskiarvoa tai korkeinta/matalinta hintaa tietylle osakkeelle kaikissa pörsseissä.
// Simuloidaan osakehintojen suoratoistoa eri pörsseistä
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();
// Käytetään scania juoksevan keskihinnan laskemiseen
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(`Juokseva keskihinta: ${averagePrice.toFixed(2)}`);
}
}
main();
Tässä esimerkissä `accumulator`-funktio laskee hintojen juoksevan kokonaissumman ja vastaanotettujen päivitysten lukumäärän. Lopullinen keskihinta lasketaan sitten näistä kertyneistä arvoista. Tämä tarjoaa reaaliaikaisen näkymän osakkeen hintaan eri maailmanlaajuisilla markkinoilla.
5. Verkkosivuston liikenteen analysointi maailmanlaajuisesti
Kuvittele maailmanlaajuinen verkkoanalytiikka-alusta, joka vastaanottaa verkkosivustovierailujen datavirtoja palvelimilta ympäri maailmaa. Jokainen datapiste edustaa käyttäjää, joka vierailee verkkosivustolla. `scan`-apuohjelman avulla voimme analysoida sivunäyttöjen trendiä maittain reaaliajassa. Oletetaan, että data näyttää tältä: `{ 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('Sivunäyttöjen määrät maittain:', counts);
}
}
main();
Tässä `accumulator`-funktio päivittää laskuria kullekin maalle. Tuloste näyttäisi kunkin maan kertyvät sivunäyttöjen määrät uusien vierailutietojen saapuessa.
`scan`-apuohjelman käytön edut
`scan`-apuohjelma tarjoaa useita etuja työskenneltäessä asynkronisten datavirtojen kanssa:
- Deklaratiivinen tyyli: `scan` antaa sinun ilmaista kumulatiivisen käsittelylogiikan deklaratiivisella ja ytimekkäällä tavalla, mikä parantaa koodin luettavuutta ja ylläpidettävyyttä.
- Asynkroninen käsittely: Se käsittelee saumattomasti asynkronisia operaatioita kertymäfunktion sisällä, mikä tekee siitä sopivan monimutkaisiin skenaarioihin, joihin liittyy I/O-sidonnaisia tehtäviä.
- Reaaliaikainen käsittely: `scan` mahdollistaa datavirtojen reaaliaikaisen käsittelyn, jolloin voit reagoida muutoksiin niiden tapahtuessa.
- Yhdisteltävyys: Sitä voidaan helposti yhdistellä muiden asynkronisten iteraattoriapuohjelmien kanssa monimutkaisten datankäsittelyputkien luomiseksi.
`scan`-apuohjelman toteuttaminen (jos se ei ole saatavilla)
Vaikka jotkut kirjastot tarjoavat sisäänrakennetun `scan`-apuohjelman, voit helposti toteuttaa oman tarvittaessa. Tässä on yksinkertainen toteutus:
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;
}
}
Tämä toteutus iteroi lähdeiteraattorin yli ja soveltaa kertymäfunktiota jokaiseen arvoon, tuottaen kertyneen tuloksen. Se käsittelee tapauksen, jossa `initialValue`-arvoa ei ole annettu, käyttämällä lähdeiteraattorin ensimmäistä arvoa alkuarvona.
Vertailu `reduce`-metodiin
On tärkeää erottaa `scan` ja `reduce`. Vaikka molemmat toimivat iteraattoreilla ja käyttävät kertymäfunktiota, ne eroavat toisistaan toiminnassaan ja tulosteessaan.
- `scan` lähettää kertyneen arvon jokaisella iteraatiolla, tarjoten juoksevan historian kertymästä.
- `reduce` lähettää vain lopullisen kertyneen arvon käsiteltyään kaikki iteraattorin elementit.
Siksi `scan` soveltuu skenaarioihin, joissa sinun on seurattava kertymän välitiloja, kun taas `reduce` on sopiva, kun tarvitset vain lopputuloksen.
Virheidenkäsittely
Kun työskennellään asynkronisten iteraattoreiden ja `scan`-apuohjelman kanssa, on ratkaisevan tärkeää käsitellä virheet siististi. Virheitä voi tapahtua iteraatioprosessin aikana tai kertymäfunktion sisällä. Voit käyttää `try...catch`-lohkoja näiden virheiden nappaamiseen ja käsittelyyn.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Jotain meni pieleen!');
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('Tapahtui virhe:', error);
}
}
main();
Tässä esimerkissä `try...catch`-lohko nappaa `generatePotentiallyFailingData`-iteraattorin heittämän virheen. Voit sitten käsitellä virheen asianmukaisesti, kuten kirjaamalla sen tai yrittämällä operaatiota uudelleen.
Yhteenveto
`scan`-apuohjelma on tehokas työkalu asynkroniseen kumulatiiviseen käsittelyyn JavaScriptin asynkronisilla iteraattoreilla. Se mahdollistaa monimutkaisten datamuunnosten ilmaisemisen deklaratiivisella ja ytimekkäällä tavalla, asynkronisten operaatioiden siistin käsittelyn ja datavirtojen reaaliaikaisen prosessoinnin. Ymmärtämällä sen toiminnallisuutta ja käyttötapauksia voit hyödyntää `scan`-apuohjelmaa rakentaaksesi vankempia ja tehokkaampia asynkronisia sovelluksia. Olitpa sitten laskemassa juoksevia summia, keräämässä dataa taulukoihin, toteuttamassa nopeusrajoittimia tai rakentamassa reaaliaikaisia datan kerääjiä, `scan` voi yksinkertaistaa koodiasi ja parantaa sen yleistä suorituskykyä. Muista ottaa huomioon virheidenkäsittely ja valita `scan` `reduce`-metodin sijaan, kun tarvitset pääsyn kertyneisiin välitila-arvoihin asynkronisten datavirtojen käsittelyn aikana. Kirjastojen, kuten RxJS:n, tutkiminen voi edelleen parantaa ymmärrystäsi ja `scan`-apuohjelman käytännön soveltamista reaktiivisen ohjelmoinnin paradigmoissa.