Uurige täiustatud JavaScripti tehnikaid samaaegseks voogude töötlemiseks. Õppige looma paralleelseid iteraatori abilisi suure läbilaskevõimega API-kõnede, failitöötluse ja andmetorude jaoks.
Suure jõudlusega JavaScripti avamine: Sügav sukeldumine iteraatori abiliste paralleelsesse töötlusse ja samaaegsetesse voogudesse
Kaasaegses tarkvaraarenduse maailmas on andmed kuningas. Seisame pidevalt silmitsi väljakutsega töödelda tohutuid andmevooge, olgu need siis pärit API-dest, andmebaasidest või failisüsteemidest. JavaScripti arendajate jaoks võib keele ühelõimelisus kujutada endast olulist kitsaskohta. Suurt andmestikku töötlev pikaajaline sünkroonne tsükkel võib brauseris kasutajaliidese külmutada või Node.js-is serveri seiskuda. Kuidas me ehitame reageerivaid ja suure jõudlusega rakendusi, mis suudavad nende intensiivsete töökoormustega tõhusalt toime tulla?
Vastus peitub asünkroonsete mustrite valdamises ja samaaegsuse omaksvõtmises. Kuigi JavaScripti tulevane Iteraatori abiliste (Iterator Helpers) ettepanek lubab revolutsiooniliselt muuta seda, kuidas me sünkroonsete kogumitega töötame, saab selle tõelise võimsuse avada, kui laiendame selle põhimõtteid asünkroonsesse maailma. See artikkel on sügav sukeldumine paralleelse töötluse kontseptsiooni iteraatorilaadsete voogude jaoks. Uurime, kuidas ehitada oma samaaegseid voooperaatoreid, et täita ülesandeid nagu suure läbilaskevõimega API-kõned ja paralleelsed andmeteisendused, muutes jõudluse kitsaskohad tõhusateks, mitteblokeerivateks torujuhtmeteks.
Alus: Iteraatorite ja iteraatori abiliste mõistmine
Enne kui saame joosta, peame õppima kõndima. Kordame lühidalt üle JavaScripti iteratsiooni põhikontseptsioonid, mis moodustavad meie täiustatud mustrite aluskivi.
Mis on iteraatori protokoll?
Iteraatori protokoll on standardne viis väärtuste jada loomiseks. Objekt on iteraator, kui sellel on next() meetod, mis tagastab objekti kahe omadusega:
value: Järjestuse järgmine väärtus.done: Tõeväärtus, mis ontrue, kui iteraator on ammendatud, ja vastasel juhulfalse.
Siin on lihtne näide kohandatud iteraatorist, mis loendab teatud arvuni:
function createCounter(limit) {
let count = 0;
return {
next: function() {
if (count < limit) {
return { value: count++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const counter = createCounter(3);
console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: undefined, done: true }
Objektid nagu massiivid, kaardid ja stringid on "itereeritavad", sest neil on [Symbol.iterator] meetod, mis tagastab iteraatori. See võimaldab meil neid kasutada for...of tsüklites.
Iteraatori abiliste lubadus
TC39 iteraatori abiliste ettepaneku eesmärk on lisada hulk abimeetodeid otse Iterator.prototype'ile. See on analoogne võimsatele meetoditele, mis meil juba on Array.prototype'il, nagu map, filter ja reduce, kuid iga itereeritava objekti jaoks. See võimaldab deklaratiivsemat ja mälu-efektiivsemat viisi jadade töötlemiseks.
Enne iteraatori abilisi (vana viis):
const numbers = [1, 2, 3, 4, 5, 6];
// Paarisarvude ruutude summa saamiseks loome vahepealseid massiive.
const evenNumbers = numbers.filter(n => n % 2 === 0);
const squares = evenNumbers.map(n => n * n);
const sum = squares.reduce((acc, n) => acc + n, 0);
console.log(sum); // 56 (2*2 + 4*4 + 6*6)
Iteraatori abilistega (kavandatud tulevik):
const numbersIterator = [1, 2, 3, 4, 5, 6].values();
// Vahepealseid massiive ei looda. Operatsioonid on laisad ja tõmmatakse ükshaaval.
const sum = numbersIterator
.filter(n => n % 2 === 0) // tagastab uue iteraatori
.map(n => n * n) // tagastab veel ühe uue iteraatori
.reduce((acc, n) => acc + n, 0); // tarbib lõpliku iteraatori
console.log(sum); // 56
Peamine järeldus on see, et need kavandatud abilised töötavad järjestikku ja sünkroonselt. Nad tõmbavad ühe elemendi, töötlevad selle läbi ahela ja seejärel tõmbavad järgmise. See on suurepärane mälutõhususe seisukohalt, kuid ei lahenda meie jõudlusprobleemi aeganõudvate, I/O-seotud operatsioonidega.
Samaaegsuse väljakutse ühelõimelises JavaScriptis
JavaScripti täitmismudel on kuulsalt ühelõimeline, keereldes ümber sündmusteahela (event loop). See tähendab, et see suudab oma põhikutsungipinul korraga täita ainult ühte koodijuppi. Kui käimas on sünkroonne, protsessorimahukas ülesanne (nagu massiivne tsükkel), blokeerib see kutsungipinu. Brauseris viib see külmunud kasutajaliideseni. Serveris tähendab see, et server ei saa vastata ühelegi teisele sissetulevale päringule.
Siin peame eristama samaaegsust (concurrency) ja paralleelsust (parallelism):
- Samaaegsus seisneb mitme ülesande haldamises aja jooksul. Sündmusteahel võimaldab JavaScriptil olla väga samaaegne. See võib alustada võrgupäringut (I/O operatsioon) ja vastust oodates saab see käsitleda kasutajaklikke või muid sündmusi. Ülesanded on põimitud, mitte ei käivitata samal ajal.
- Paralleelsus seisneb mitme ülesande käivitamises täpselt samal ajal. Tõeline paralleelsus JavaScriptis saavutatakse tavaliselt tehnoloogiate abil nagu Web Workers brauseris või Worker Threads/Child Processes Node.js-is, mis pakuvad eraldi lõimi oma sündmusteahelatega.
Oma eesmärkidel keskendume kõrge samaaegsuse saavutamisele I/O-seotud operatsioonide (nagu API-kõned) puhul, kus sageli leitakse kõige olulisemad reaalse maailma jõudluse kasvud.
Paradigma nihe: asünkroonsed iteraatorid
Andmevoogude käsitlemiseks, mis saabuvad aja jooksul (näiteks võrgupäringust või suurest failist), tutvustas JavaScript asünkroonse iteraatori protokolli. See on väga sarnane oma sünkroonse sugulasega, kuid ühe olulise erinevusega: next() meetod tagastab Promise'i, mis lahendub { value, done } objektiks.
See võimaldab meil töötada andmeallikatega, millel pole kogu teave korraga saadaval. Nende asünkroonsete voogude sujuvaks tarbimiseks kasutame for await...of tsüklit.
Loome asünkroonse iteraatori, mis simuleerib andmete lehekülgede kaupa toomist API-st:
async function* fetchPaginatedData(url) {
let nextPageUrl = url;
while (nextPageUrl) {
console.log(`Pärin andmeid aadressilt ${nextPageUrl}...`);
const response = await fetch(nextPageUrl);
if (!response.ok) {
throw new Error(`API päring ebaõnnestus staatusega ${response.status}`);
}
const data = await response.json();
// Väljasta iga element praeguse lehe tulemustest
for (const item of data.results) {
yield item;
}
// Liigu järgmisele lehele või peatu, kui seda pole
nextPageUrl = data.nextPage;
}
}
// Kasutus:
async function processUsers() {
const userStream = fetchPaginatedData('https://api.example.com/users');
for await (const user of userStream) {
console.log(`Töötlen kasutajat: ${user.name}`);
// See on endiselt järjestikune töötlus. Ootame ühe kasutaja logimise ära
// enne, kui järgmist isegi voost küsitakse.
}
}
See on võimas muster, kuid pange tähele kommentaari tsüklis. Töötlus on järjestikune. Kui `kasutaja töötlemine` hõlmaks teist aeglast, asünkroonset operatsiooni (näiteks andmebaasi salvestamine), ootaksime igaühe lõpuleviimist enne järgmise alustamist. See on kitsaskoht, mille me tahame kõrvaldada.
Samaaegsete voooperatsioonide arhitektuuri loomine iteraatori abilistega
Nüüd jõuame oma arutelu tuumani. Kuidas saame asünkroonsest voost elemente töödelda samaaegselt, ootamata eelmise elemendi lõppemist? Ehitame kohandatud asünkroonse iteraatori abilise, nimetagem seda asyncMapConcurrent.
See funktsioon võtab kolm argumenti:
sourceIterator: Asünkroonne iteraator, millest me tahame elemente tõmmata.mapperFn: Asünkroonne funktsioon, mida rakendatakse igale elemendile.concurrency: Arv, mis määrab, mitu `mapperFn` operatsiooni saab samal ajal käivitada.
Põhikontseptsioon: lubaduste (Promise) tööliste kogum
Strateegia on säilitada aktiivsete lubaduste "kogum" või hulk. Selle kogumi suurus on piiratud meie concurrency parameetriga.
- Alustame elementide tõmbamisega lähteiteraatorist ja algatame nende jaoks asünkroonse `mapperFn`-i.
- Lisame `mapperFn`-i poolt tagastatud lubaduse meie aktiivsesse kogumisse.
- Jätkame seda, kuni kogum on täis (selle suurus võrdub meie `concurrency` tasemega).
- Kui kogum on täis, selle asemel et oodata *kõiki* lubadusi, kasutame
Promise.race(), et oodata vaid *ühe* neist lõpuleviimist. - Kui lubadus on lõpule viidud, väljastame selle tulemuse, eemaldame selle kogumist ja nüüd on ruumi uue lisamiseks.
- Tõmbame lähteallikast järgmise elemendi, alustame selle töötlemist, lisame uue lubaduse kogumisse ja kordame tsüklit.
See loob pideva voo, kus tööd tehakse alati kuni määratletud samaaegsuse piirini, tagades, et meie töötlustoru ei ole kunagi jõude, seni kuni on andmeid, mida töödelda.
`asyncMapConcurrent` samm-sammuline implementatsioon
Ehitame selle utiliidi. See on asünkroonne generaatorfunktsioon, mis teeb asünkroonse iteraatori protokolli implementeerimise lihtsaks.
async function* asyncMapConcurrent(sourceIterator, mapperFn, concurrency = 5) {
const activePromises = new Set();
const source = sourceIterator[Symbol.asyncIterator]();
while (true) {
// 1. Täida kogum kuni samaaegsuse piirini
while (activePromises.size < concurrency) {
const { value, done } = await source.next();
if (done) {
// Lähteiteraator on ammendatud, katkesta sisemine tsükkel
break;
}
const promise = (async () => {
try {
return { result: await mapperFn(value), error: null };
} catch (e) {
return { result: null, error: e };
}
})();
activePromises.add(promise);
// Samuti, lisa lubadusele puhastusfunktsioon, et see lõpetamisel hulgast eemaldada.
promise.finally(() => activePromises.delete(promise));
}
// 2. Kontrolli, kas oleme valmis
if (activePromises.size === 0) {
// Lähteallikas on ammendatud ja kõik aktiivsed lubadused on lõppenud.
return; // Lõpeta generaator
}
// 3. Oota, kuni mõni lubadus kogumis lõpeb
const completed = await Promise.race(activePromises);
// 4. Käsitle tulemust
if (completed.error) {
// Saame otsustada veakäsitlusstrateegia üle. Siin me viskame vea uuesti.
throw completed.error;
}
// 5. Väljasta edukas tulemus
yield completed.result;
}
}
Vaatame implementatsiooni lähemalt:
- Kasutame
activePromisesjaoksSet'i. Hulgad on mugavad unikaalsete objektide (nagu lubadused) hoidmiseks ning pakuvad kiiret lisamist ja kustutamist. - Välimine
while (true)tsükkel hoiab protsessi käimas, kuni me sellest selgesõnaliselt väljume. - Sisemine
while (activePromises.size < concurrency)tsükkel vastutab meie tööliste kogumi täitmise eest. See tõmbab pidevaltsourceiteraatorist. - Kui lähteiteraator on
done, lõpetame uute lubaduste lisamise. - Iga uue elemendi puhul kutsume kohe välja asünkroonse IIFE (Immediately Invoked Function Expression). See alustab
mapperFntäitmist otsekohe. Me mähime selle `try...catch` plokki, et sujuvalt käsitleda võimalikke vigu teisendajast ja tagastada ühtlane objektikuju{ result, error }. - Otsustava tähtsusega on, et me kasutame
promise.finally(() => activePromises.delete(promise)). See tagab, et olenemata sellest, kas lubadus laheneb või lükatakse tagasi, eemaldatakse see meie aktiivsest hulgast, tehes ruumi uuele tööle. See on puhtam lähenemine kui püüda lubadust käsitsi leida ja eemaldada pärast `Promise.race`. Promise.race(activePromises)on samaaegsuse süda. See tagastab uue lubaduse, mis laheneb või lükatakse tagasi niipea, kui *esimene* lubadus hulgas seda teeb.- Kui lubadus on lõpule viidud, uurime oma mähitud tulemust. Kui esineb viga, viskame selle, lõpetades generaatori (kiire ebaõnnestumise strateegia). Kui see on edukas,
yield'ime tulemuse meieasyncMapConcurrentgeneraatori tarbijale. - Lõplik väljumistingimus on siis, kui lähteallikas on ammendatud ja
activePromiseshulk tühjeneb. Sel hetkel on välimise tsükli tingimusactivePromises.size === 0täidetud ja me kasutamereturn, mis annab märku meie asünkroonse generaatori lõpust.
Praktilised kasutusjuhud ja globaalsed näited
See muster pole pelgalt akadeemiline harjutus. Sellel on sügavad tagajärjed reaalsetes rakendustes. Uurime mõningaid stsenaariume.
Kasutusjuht 1: Suure läbilaskevõimega API interaktsioonid
Stsenaarium: Kujutage ette, et ehitate teenust globaalsele e-kaubanduse platvormile. Teil on nimekiri 50 000 toote ID-st ja igaühe jaoks peate kutsuma hinnastamise API-d, et saada uusim hind konkreetse piirkonna jaoks.
Järjestikune kitsaskoht:
async function updateAllPrices(productIds) {
const startTime = Date.now();
for (const id of productIds) {
await fetchPrice(id); // Assume this takes ~200ms
}
console.log(`Koguaeg: ${(Date.now() - startTime) / 1000}s`);
}
// Hinnanguline aeg 50 000 toote jaoks: 50 000 * 0.2s = 10 000 sekundit (~2,7 tundi!)
Samaaegne lahendus:
// Abifunktsioon võrgupäringu simuleerimiseks
function fetchPrice(productId) {
return new Promise(resolve => {
setTimeout(() => {
const price = (Math.random() * 100).toFixed(2);
console.log(`Tõmmatud hind tootele ${productId}: $${price}`);
resolve({ productId, price });
}, 200 + Math.random() * 100); // Simuleeri muutuvat võrgulatentsust
});
}
async function updateAllPricesConcurrently() {
const productIds = Array.from({ length: 50 }, (_, i) => `product-${i + 1}`);
const idIterator = productIds.values(); // Loo lihtne iteraator
// Kasuta meie samaaegset teisendajat samaaegsusega 10
const priceStream = asyncMapConcurrent(idIterator, fetchPrice, 10);
const startTime = Date.now();
for await (const priceData of priceStream) {
// Siin salvestaksite priceData oma andmebaasi
// console.log(`Processed: ${priceData.productId}`);
}
console.log(`Samaaegne koguaeg: ${(Date.now() - startTime) / 1000}s`);
}
updateAllPricesConcurrently();
// Oodatav väljund: "Fetched price..." logide tulv ja koguaeg,
// mis on umbes (Elementide koguarv / Samaaegsus) * Keskmine aeg elemendi kohta.
// 50 elemendi puhul 200ms ja samaaegsusega 10: (50/10) * 0.2s = ~1 sekund (pluss latentsuse varieeruvus)
// 50 000 elemendi puhul: (50000/10) * 0.2s = 1000 sekundit (~16,7 minutit). Tohutu edasiminek!
Globaalne kaalutlus: Olge teadlik API päringulimiitidest. Liiga kõrge samaaegsuse taseme seadmine võib teie IP-aadressi blokeerida. Samaaegsus 5–10 on paljude avalike API-de jaoks sageli turvaline lähtepunkt.
Kasutusjuht 2: Paralleelne failitöötlus Node.js-is
Stsenaarium: Ehitate sisuhaldussüsteemi (CMS), mis võtab vastu hulgi piltide üleslaadimisi. Iga üleslaaditud pildi jaoks peate genereerima kolm erineva suurusega pisipilti ja laadima need üles pilvemäluteenuse pakkujale nagu AWS S3 või Google Cloud Storage.
Järjestikune kitsaskoht: Ühe pildi täielik töötlemine (lugemine, kolm korda suuruse muutmine, kolm korda üleslaadimine) enne järgmise alustamist on väga ebaefektiivne. See alakasutab nii protsessorit (üleslaadimiste I/O ooteaegadel) kui ka võrku (protsessorimahuka suuruse muutmise ajal).
Samaaegne lahendus:
const fs = require('fs/promises');
const path = require('path');
// Eeldame, et 'sharp' suuruse muutmiseks ja 'aws-sdk' üleslaadimiseks on saadaval
async function processImage(filePath) {
console.log(`Töötlen faili ${path.basename(filePath)}...`);
const imageBuffer = await fs.readFile(filePath);
const sizes = [{w: 100, h: 100}, {w: 300, h: 300}, {w: 600, h: 600}];
const uploadTasks = sizes.map(async (size) => {
const thumbnailBuffer = await sharp(imageBuffer).resize(size.w, size.h).toBuffer();
return uploadToCloud(thumbnailBuffer, `thumb_${size.w}_${path.basename(filePath)}`);
});
await Promise.all(uploadTasks);
console.log(`Lõpetatud: ${path.basename(filePath)}`);
return { source: filePath, status: 'processed' };
}
async function run() {
const imageDir = './uploads';
const files = await fs.readdir(imageDir);
const filePaths = files.map(f => path.join(imageDir, f));
// Hangi protsessori tuumade arv mõistliku samaaegsuse taseme määramiseks
const concurrency = require('os').cpus().length;
const processingStream = asyncMapConcurrent(filePaths.values(), processImage, concurrency);
for await (const result of processingStream) {
console.log(result);
}
}
Selles näites seame samaaegsuse taseme võrdseks saadaolevate protsessori tuumade arvuga. See on tavaline heuristika protsessorimahukate ülesannete jaoks, tagades, et me ei küllasta süsteemi üleliigse tööga, mida see paralleelselt hallata ei suuda.
Jõudluse kaalutlused ja parimad praktikad
Samaaegsuse rakendamine on võimas, kuid see pole imerohi. See lisab keerukust ja nõuab hoolikat kaalumist.
Õige samaaegsuse taseme valimine
Optimaalne samaaegsuse tase ei ole alati "nii kõrge kui võimalik". See sõltub ülesande olemusest:
- I/O-seotud ülesanded (nt API-kõned, andmebaasipäringud): Teie kood veedab suurema osa ajast väliste ressursside ootamisele. Sageli saate kasutada kõrgemat samaaegsuse taset (nt 10, 50 või isegi 100), mida piiravad peamiselt välise teenuse päringulimiidid ja teie enda võrgu ribalaius.
- Protsessorimahukad ülesanded (nt pilditöötlus, keerulised arvutused, krüpteerimine): Teie koodi piirab teie masina töötlemisvõimsus. Hea lähtepunkt on seada samaaegsuse tase võrdseks saadaolevate protsessori tuumade arvuga (
navigator.hardwareConcurrencybrauserites,os.cpus().lengthNode.js-is). Selle palju kõrgemaks seadmine võib põhjustada liigset kontekstivahetust, mis võib tegelikult jõudlust aeglustada.
Veakäsitlus samaaegsetes voogudes
Meie praegusel implementatsioonil on "kiire ebaõnnestumise" strateegia. Kui mõni `mapperFn` viskab vea, lõpeb kogu voog. See võib olla soovitav, kuid sageli soovite jätkata teiste elementide töötlemist. Saate abilist muuta, et koguda ebaõnnestumisi ja väljastada need eraldi, või lihtsalt logida need ja edasi liikuda.
Tugevam versioon võib välja näha selline:
// Generaatori muudetud osa
const completed = await Promise.race(activePromises);
if (completed.error) {
console.error("Samaaegses ülesandes ilmnes viga:", completed.error);
// Me ei viska viga, vaid jätkame tsüklit, et oodata järgmist lubadust.
// Võiksime ka vea väljastada, et tarbija saaks seda käsitleda.
// yield { error: completed.error };
} else {
yield completed.result;
}
Vasturõhu haldamine (Backpressure)
Vasturõhk on voogude töötlemisel kriitiline kontseptsioon. See on see, mis juhtub, kui kiiresti tootev andmeallikas koormab üle aeglase tarbija. Meie tõmbepõhise iteraatori lähenemise ilu seisneb selles, et see haldab vasturõhku automaatselt. Meie asyncMapConcurrent funktsioon tõmbab uue elemendi sourceIterator'ist ainult siis, kui activePromises kogumis on vaba koht. Kui meie voo tarbija on väljastatud tulemuste töötlemisel aeglane, peatub meie generaator ja omakorda lõpetab allikast tõmbamise. See hoiab ära mälu ammendumise tohutu hulga töötlemata elementide puhverdamise tõttu.
Tulemuste järjekord
Samaaegse töötluse oluline tagajärg on see, et tulemused väljastatakse lõpetamise järjekorras, mitte lähteandmete algses järjekorras. Kui teie lähtenimekirja kolmas element on väga kiiresti töödeldav ja esimene väga aeglane, saate esimesena kolmanda elemendi tulemuse. Kui algse järjekorra säilitamine on nõue, peate ehitama keerukama lahenduse, mis hõlmab puhverdamist ja tulemuste ümberjärjestamist, mis lisab olulist mälukulu.
Tulevik: natiivsed implementatsioonid ja ökosüsteem
Kuigi oma samaaegse abilise ehitamine on fantastiline õpikogemus, pakub JavaScripti ökosüsteem nende ülesannete jaoks tugevaid, lahingus testitud teeke.
- p-map: Populaarne ja kergekaaluline teek, mis teeb täpselt seda, mida meie
asyncMapConcurrent, kuid rohkemate funktsioonide ja optimeerimistega. - RxJS: Võimas teek reaktiivseks programmeerimiseks jälgitavatega (observables), mis on nagu supervõimetega vood. Sellel on operaatorid nagu
mergeMap, mida saab konfigureerida samaaegseks täitmiseks. - Node.js Streams API: Serveripoolsete rakenduste jaoks pakuvad Node.js-i vood võimsaid, vasturõhust teadlikke torujuhtmeid, kuigi nende API võib olla keerulisem omandada.
Kuna JavaScripti keel areneb, on võimalik, et ühel päeval näeme natiivset Iterator.prototype.mapConcurrent'i või sarnast utiliiti. Arutelud TC39 komitees näitavad selget suundumust pakkuda arendajatele võimsamaid ja ergonoomilisemaid tööriistu andmevoogude käsitlemiseks. Aluspõhimõtete mõistmine, nagu me oleme selles artiklis teinud, tagab, et olete valmis neid tööriistu nende saabumisel tõhusalt ära kasutama.
Kokkuvõte
Oleme rännanud JavaScripti iteraatorite põhitõdedest samaaegse voogude töötlemise utiliidi keeruka arhitektuurini. See teekond paljastab võimsa tõe kaasaegse JavaScripti arenduse kohta: jõudlus ei seisne ainult ühe funktsiooni optimeerimises, vaid tõhusate andmevoogude arhitektuuri loomises.
Põhilised järeldused:
- Standardsed iteraatori abilised on sünkroonsed ja järjestikused.
- Asünkroonsed iteraatorid ja
for await...ofpakuvad puhast süntaksit andmevoogude töötlemiseks, kuid jäävad vaikimisi järjestikusteks. - Tõelised jõudluse kasvud I/O-seotud ülesannete jaoks tulevad samaaegsusest – mitme elemendi korraga töötlemisest.
- Lubaduste "tööliste kogum", mida hallatakse
Promise.race'iga, on tõhus muster samaaegsete teisendajate ehitamiseks. - See muster pakub sisseehitatud vasturõhu haldamist, vältides mälu ülekoormust.
- Paralleelse töötluse rakendamisel olge alati teadlik samaaegsuse piirangutest, veakäsitlusest ja tulemuste järjestusest.
Liikudes kaugemale lihtsatest tsüklitest ja võttes omaks need täiustatud, samaaegsed voogude mustrid, saate ehitada JavaScripti rakendusi, mis pole mitte ainult jõudsamad ja skaleeritavamad, vaid ka vastupidavamad suurte andmetöötluse väljakutsete ees. Olete nüüd varustatud teadmistega, et muuta andmete kitsaskohad kiireteks torujuhtmeteks, mis on tänapäeva andmepõhises maailmas iga arendaja jaoks kriitiline oskus.