LÄs upp kraften i parallell bearbetning i JavaScript. LÀr dig hantera samtidiga Promises med Promise.all, allSettled, race och any för snabbare, mer robusta applikationer.
BemÀstra JavaScript Concurrency: En djupdykning i parallell bearbetning av Promises
I landskapet för modern webbutveckling Àr prestanda inte en funktion; det Àr ett grundlÀggande krav. AnvÀndare över hela vÀrlden förvÀntar sig att applikationer ska vara snabba, responsiva och sömlösa. KÀrnan i denna prestandautmaning, sÀrskilt i JavaScript, Àr konceptet att hantera asynkrona operationer effektivt. FrÄn att hÀmta data frÄn ett API till att lÀsa en fil eller köra en databasfrÄga, Àr det mÄnga uppgifter som inte slutförs omedelbart. Hur vi hanterar dessa vÀntetider kan vara skillnaden mellan en trög applikation och en hÀrligt smidig anvÀndarupplevelse.
JavaScript Ă€r till sin natur ett entrĂ„dat sprĂ„k. Detta innebĂ€r att det bara kan exekvera en koddel Ă„t gĂ„ngen. Detta kan lĂ„ta som en begrĂ€nsning, men JavaScripts hĂ€ndelseloop (event loop) och icke-blockerande I/O-modell gör att det kan hantera asynkrona uppgifter med otrolig effektivitet. Den moderna hörnstenen i denna modell Ă€r Promise â ett objekt som representerar det slutgiltiga slutförandet (eller misslyckandet) av en asynkron operation.
Men att bara anvÀnda Promises eller deras eleganta `async/await`-syntax garanterar inte automatiskt optimal prestanda. En vanlig fallgrop för utvecklare Àr att hantera flera oberoende asynkrona uppgifter sekventiellt, vilket skapar onödiga flaskhalsar. Det Àr hÀr samtidig bearbetning av promises kommer in i bilden. Genom att starta flera asynkrona operationer parallellt och vÀnta pÄ dem kollektivt kan vi dramatiskt minska den totala exekveringstiden och bygga mycket effektivare applikationer.
Denna omfattande guide tar dig med pĂ„ en djupdykning i vĂ€rlden av JavaScript concurrency. Vi kommer att utforska verktygen som Ă€r inbyggda direkt i sprĂ„ket â `Promise.all()`, `Promise.allSettled()`, `Promise.race()` och `Promise.any()` â för att hjĂ€lpa dig att orkestrera parallella uppgifter som ett proffs. Oavsett om du Ă€r en junior utvecklare som hĂ„ller pĂ„ att bekanta dig med asynkronicitet eller en erfaren ingenjör som vill finslipa dina mönster, kommer denna artikel att utrusta dig med kunskapen för att skriva snabbare, mer motstĂ„ndskraftig och mer sofistikerad JavaScript-kod.
Först, ett snabbt förtydligande: Concurrency vs. Parallellism
Innan vi fortsÀtter Àr det viktigt att klargöra tvÄ termer som ofta anvÀnds omvÀxlande men som har distinkta betydelser inom datavetenskap: concurrency och parallellism.
- Concurrency (samtidighet) Àr konceptet att hantera flera uppgifter under en tidsperiod. Det handlar om att hantera mÄnga saker samtidigt. Ett system Àr 'concurrent' om det kan starta, köra och slutföra mer Àn en uppgift utan att vÀnta pÄ att den föregÄende ska avslutas. I JavaScripts entrÄdsmiljö uppnÄs concurrency via hÀndelseloopen, som lÄter motorn vÀxla mellan uppgifter. Medan en lÄngvarig uppgift (som en nÀtverksförfrÄgan) vÀntar, kan motorn arbeta med andra saker.
- Parallellism Ă€r konceptet att exekvera flera uppgifter samtidigt. Det handlar om att göra mĂ„nga saker pĂ„ en gĂ„ng. Sann parallellism krĂ€ver en flerkĂ€rnig processor, dĂ€r olika trĂ„dar kan köras pĂ„ olika kĂ€rnor exakt samtidigt. Ăven om web workers möjliggör sann parallellism i webblĂ€sarbaserad JavaScript, avser den centrala concurrency-modellen vi diskuterar hĂ€r den enda huvudtrĂ„den.
För I/O-bundna operationer (som nÀtverksförfrÄgningar) ger JavaScripts 'concurrent'-modell *effekten* av parallellism. Vi kan initiera flera förfrÄgningar samtidigt. Medan JavaScript-motorn vÀntar pÄ svaren Àr den fri att utföra annat arbete. Operationerna sker 'parallellt' ur de externa resursernas (servrar, filsystem) perspektiv. Detta Àr den kraftfulla modell vi kommer att utnyttja.
Den sekventiella fÀllan: Ett vanligt anti-mönster
LÄt oss börja med att identifiera ett vanligt misstag. NÀr utvecklare först lÀr sig `async/await` Àr syntaxen sÄ ren att det Àr lÀtt att skriva kod som ser synkron ut men som oavsiktligt Àr sekventiell och ineffektiv. FörestÀll dig att du behöver hÀmta en anvÀndares profil, deras senaste inlÀgg och deras aviseringar för att bygga en instrumentpanel.
Ett naivt tillvÀgagÄngssÀtt kan se ut sÄ hÀr:
Exempel: Den ineffektiva sekventiella hÀmtningen
async function fetchDashboardDataSequentially(userId) {
console.time('sequentialFetch');
console.log('Fetching user profile...');
const userProfile = await fetchUserProfile(userId); // Waits here
console.log('Fetching user posts...');
const userPosts = await fetchUserPosts(userId); // Waits here
console.log('Fetching user notifications...');
const userNotifications = await fetchUserNotifications(userId); // Waits here
console.timeEnd('sequentialFetch');
return { userProfile, userPosts, userNotifications };
}
// Imagine these functions take time to resolve
// fetchUserProfile -> 500ms
// fetchUserPosts -> 800ms
// fetchUserNotifications -> 1000ms
Vad Àr fel med den hÀr bilden? Varje `await`-nyckelord pausar exekveringen av funktionen `fetchDashboardDataSequentially` tills det aktuella promise-objektet har lösts. FörfrÄgan för `userPosts` startar inte ens förrÀn `userProfile`-förfrÄgan Àr helt klar. FörfrÄgan för `userNotifications` startar inte förrÀn `userPosts` har returnerats. Dessa tre nÀtverksförfrÄgningar Àr oberoende av varandra; det finns ingen anledning att vÀnta! Den totala tiden blir summan av alla individuella tider:
Total Time â 500ms + 800ms + 1000ms = 2300ms
Detta Àr en enorm prestandaflaskhals. Vi kan göra mycket, mycket bÀttre.
LÄs upp prestanda: Kraften i samtidig exekvering
Lösningen Àr att initiera alla asynkrona operationer pÄ en gÄng, utan att omedelbart invÀnta dem med `await`. Detta lÄter dem köras samtidigt. Vi kan lagra de vÀntande Promise-objekten i variabler och sedan anvÀnda en Promise-kombinator för att vÀnta pÄ att alla ska slutföras.
Exempel: Den effektiva samtidiga hÀmtningen
async function fetchDashboardDataConcurrently(userId) {
console.time('concurrentFetch');
console.log('Initiating all fetches at once...');
const profilePromise = fetchUserProfile(userId);
const postsPromise = fetchUserPosts(userId);
const notificationsPromise = fetchUserNotifications(userId);
// Now we wait for all of them to complete
const [userProfile, userPosts, userNotifications] = await Promise.all([
profilePromise,
postsPromise,
notificationsPromise
]);
console.timeEnd('concurrentFetch');
return { userProfile, userPosts, userNotifications };
}
I denna version anropar vi de tre hÀmtningsfunktionerna utan `await`. Detta startar omedelbart alla tre nÀtverksförfrÄgningar. JavaScript-motorn lÀmnar över dem till den underliggande miljön (webblÀsaren eller Node.js) och fÄr tillbaka tre vÀntande Promises. Sedan anvÀnds `Promise.all()` för att vÀnta pÄ att alla tre av dessa promises ska lösas. Den totala tiden bestÀms nu av den operation som tar lÀngst tid, inte summan.
Total Time â max(500ms, 800ms, 1000ms) = 1000ms
Vi har precis mer Àn halverat vÄr datahÀmtningstid! Detta Àr den grundlÀggande principen för parallell bearbetning av promises. LÄt oss nu utforska de kraftfulla verktyg som JavaScript erbjuder för att orkestrera dessa samtidiga uppgifter.
VerktygslÄdan för Promise-kombinatorer: `all`, `allSettled`, `race` och `any`
JavaScript tillhandahÄller fyra statiska metoder pÄ `Promise`-objektet, kÀnda som promise-kombinatorer. Var och en tar en itererbar samling (som en array) av promises och returnerar ett enda nytt promise. Beteendet hos detta nya promise beror pÄ vilken kombinator du anvÀnder.
1. `Promise.all()`: Allt-eller-inget-metoden
`Promise.all()` Àr det perfekta verktyget nÀr du har en grupp uppgifter som alla Àr kritiska för nÀsta steg. Det representerar det logiska "OCH"-villkoret: Uppgift 1 OCH Uppgift 2 OCH Uppgift 3 mÄste alla lyckas.
- Indata: En itererbar samling av promises.
- Beteende: Den returnerar ett enda promise som uppfylls (fulfills) nÀr alla ingÄende promises har uppfyllts. Det uppfyllda vÀrdet Àr en array med resultaten frÄn de ingÄende promises, i samma ordning.
- FellÀge: Den avvisar (rejects) omedelbart sÄ snart nÄgot av de ingÄende promises avvisas. Anledningen till avvisningen Àr anledningen frÄn det första promise som avvisades. Detta kallas ofta för "fail-fast"-beteende.
AnvÀndningsfall: Aggregering av kritisk data
VÄrt instrumentpanelsexempel Àr ett perfekt anvÀndningsfall. Om du inte kan ladda anvÀndarens profil, kanske det inte Àr meningsfullt att visa deras inlÀgg och aviseringar. Hela komponenten Àr beroende av att alla tre datapunkter Àr tillgÀngliga.
// Helper to simulate API calls
const mockApiCall = (value, delay, shouldFail = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`API call failed for: ${value}`));
} else {
console.log(`Resolved: ${value}`);
resolve({ data: value });
}
}, delay);
});
};
async function loadCriticalData() {
console.log('Using Promise.all for critical data...');
try {
const [profile, settings, permissions] = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700),
mockApiCall('userPermissions', 500)
]);
console.log('All critical data loaded successfully!');
// Now render the UI with profile, settings, and permissions
} catch (error) {
console.error('Failed to load critical data:', error.message);
// Show an error message to the user
}
}
// What happens if one fails?
async function loadCriticalDataWithFailure() {
console.log('\nDemonstrating Promise.all failure...');
try {
const results = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700, true), // This one will fail
mockApiCall('userPermissions', 500)
]);
} catch (error) {
console.error('Promise.all rejected:', error.message);
// Note: The 'userProfile' and 'userPermissions' calls may have completed,
// but their results are lost because the whole operation failed.
}
}
loadCriticalData();
// After a delay, call the failure example
setTimeout(loadCriticalDataWithFailure, 2000);
Fallgrop med `Promise.all()`
Den primÀra fallgropen Àr dess fail-fast-natur. Om du hÀmtar data för tio olika, oberoende widgets pÄ en sida, och ett API misslyckas, kommer `Promise.all()` att avvisas, och du förlorar resultaten för de andra nio lyckade anropen. Det Àr hÀr vÄr nÀsta kombinator briljerar.
2. `Promise.allSettled()`: Den motstÄndskraftiga insamlaren
Introducerad i ES2020, var `Promise.allSettled()` en revolution för motstÄndskraft. Den Àr designad för nÀr du vill veta utfallet av varje enskilt promise, oavsett om det lyckades eller misslyckades. Den avvisas aldrig.
- Indata: En itererbar samling av promises.
- Beteende: Den returnerar ett enda promise som alltid uppfylls. Det uppfylls nÀr alla ingÄende promises har avgjorts (settled), antingen uppfyllts eller avvisats. Det uppfyllda vÀrdet Àr en array av objekt, dÀr varje objekt beskriver utfallet av ett promise.
- Resultatformat: Varje resultatobjekt har en `status`-egenskap.
- Om uppfyllt: `{ status: 'fulfilled', value: theResult }`
- Om avvisat: `{ status: 'rejected', reason: theError }`
AnvÀndningsfall: Icke-kritiska, oberoende operationer
FörestÀll dig en sida som visar flera oberoende komponenter: en vÀder-widget, ett nyhetsflöde och en aktiekurs-ticker. Om nyhetsflödets API misslyckas vill du fortfarande visa vÀder- och aktieinformationen. `Promise.allSettled()` Àr perfekt för detta.
async function loadDashboardWidgets() {
console.log('\nUsing Promise.allSettled for independent widgets...');
const results = await Promise.allSettled([
mockApiCall('Weather Data', 600),
mockApiCall('News Feed', 1200, true), // This API is down
mockApiCall('Stock Ticker', 800)
]);
console.log('All promises have settled. Processing results...');
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Widget ${index} loaded successfully with data:`, result.value.data);
// Render this widget to the UI
} else {
console.error(`Widget ${index} failed to load:`, result.reason.message);
// Show a specific error state for this widget
}
});
}
loadDashboardWidgets();
Med `Promise.allSettled()` blir din applikation mycket mer robust. En enskild felpunkt orsakar inte en kaskad som river ner hela anvÀndargrÀnssnittet. Du kan hantera varje utfall pÄ ett elegant sÀtt.
3. `Promise.race()`: Först över mÄllinjen
`Promise.race()` gör exakt vad namnet antyder. Den stÀller en grupp promises mot varandra och utser en vinnare sÄ snart den första korsar mÄllinjen, oavsett om det var en framgÄng eller ett misslyckande.
- Indata: En itererbar samling av promises.
- Beteende: Den returnerar ett enda promise som avgörs (uppfylls eller avvisas) sÄ snart det första av de ingÄende promises avgörs. UppfyllnadsvÀrdet eller avvisningsanledningen för det returnerade promise-objektet blir detsamma som för det "vinnande" promise-objektet.
- Viktig notering: De andra promises avbryts inte. De kommer att fortsÀtta köras i bakgrunden, och deras resultat kommer helt enkelt att ignoreras av `Promise.race()`-kontexten.
AnvÀndningsfall: Implementera en timeout
Det vanligaste och mest praktiska anvÀndningsfallet för `Promise.race()` Àr att tvinga fram en timeout pÄ en asynkron operation. Du kan lÄta din huvudoperation "tÀvla" mot ett `setTimeout`-promise. Om din operation tar för lÄng tid kommer timeout-promise att avgöras först, och du kan hantera det som ett fel.
function createTimeout(delay) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${delay}ms`));
}, delay);
});
}
async function fetchDataWithTimeout() {
console.log('\nUsing Promise.race for a timeout...');
try {
const result = await Promise.race([
mockApiCall('some critical data', 2000), // This will take too long
createTimeout(1500) // This will win the race
]);
console.log('Data fetched successfully:', result.data);
} catch (error) {
console.error(error.message);
}
}
fetchDataWithTimeout();
Ett annat anvÀndningsfall: Redundanta Àndpunkter
Du kan ocksÄ anvÀnda `Promise.race()` för att frÄga flera redundanta servrar om samma resurs och ta emot svaret frÄn den server som Àr snabbast. Detta Àr dock riskabelt, för om den snabbaste servern returnerar ett fel (t.ex. en 500-statuskod), kommer `Promise.race()` att avvisas omedelbart, Àven om en nÄgot lÄngsammare server skulle ha returnerat ett lyckat svar. Detta leder oss till vÄr sista, mer lÀmpliga kombinator för detta scenario.
4. `Promise.any()`: Den första som lyckas
Introducerad i ES2021, Àr `Promise.any()` som en mer optimistisk version av `Promise.race()`. Den vÀntar ocksÄ pÄ att det första promise-objektet ska avgöras, men den letar specifikt efter det första som uppfylls.
- Indata: En itererbar samling av promises.
- Beteende: Den returnerar ett enda promise som uppfylls sÄ snart nÄgot av de ingÄende promises uppfylls. UppfyllnadsvÀrdet Àr vÀrdet frÄn det första promise som uppfylldes.
- FellĂ€ge: Den avvisas endast om alla ingĂ„ende promises avvisas. Avvisningsanledningen Ă€r ett speciellt `AggregateError`-objekt, som innehĂ„ller en `errors`-egenskap â en array med alla individuella avvisningsanledningar.
AnvÀndningsfall: HÀmtning frÄn redundanta kÀllor
Detta Àr det perfekta verktyget för att hÀmta en resurs frÄn flera kÀllor, som primÀra och backup-servrar eller flera Content Delivery Networks (CDN). Du bryr dig bara om att fÄ ett lyckat svar sÄ snabbt som möjligt.
async function fetchResourceFromMirrors() {
console.log('\nUsing Promise.any to find the fastest successful source...');
try {
const resource = await Promise.any([
mockApiCall('Primary CDN', 800, true), // Fails quickly
mockApiCall('European Mirror', 1200), // Slower but will succeed
mockApiCall('Asian Mirror', 1100) // Also succeeds, but is slower than the European one
]);
console.log('Resource fetched successfully from a mirror:', resource.data);
} catch (error) {
if (error instanceof AggregateError) {
console.error('All mirrors failed to provide the resource.');
// You can inspect individual errors:
error.errors.forEach(err => console.log('- ' + err.message));
}
}
}
fetchResourceFromMirrors();
I det hÀr exemplet kommer `Promise.any()` att ignorera det snabba misslyckandet frÄn den primÀra CDN:en och vÀnta pÄ att den europeiska spegelservern (European Mirror) ska uppfyllas, varpÄ den kommer att lösas med den datan och i praktiken ignorera resultatet frÄn den asiatiska spegelservern.
Att vÀlja rÀtt verktyg för jobbet: En snabbguide
Med fyra kraftfulla alternativ, hur bestÀmmer du dig för vilket du ska anvÀnda? HÀr Àr ett enkelt ramverk för beslutsfattande:
- Behöver jag resultaten frĂ„n ALLA promises, och Ă€r det en katastrof om NĂ
GOT av dem misslyckas?
AnvÀndPromise.all(). Detta Àr för tÀtt kopplade allt-eller-inget-scenarier. - Behöver jag veta utfallet av ALLA promises, oavsett om de lyckas eller misslyckas?
AnvÀndPromise.allSettled(). Detta Àr för hantering av flera oberoende uppgifter dÀr du vill bearbeta varje utfall och bibehÄlla applikationens motstÄndskraft. - Bryr jag mig bara om det allra första promise-objektet som blir klart, oavsett om det Àr en framgÄng eller ett misslyckande?
AnvÀndPromise.race(). Detta Àr primÀrt för att implementera timeouts eller andra kapplöpningssituationer dÀr det första resultatet (av vilket slag som helst) Àr det enda som spelar roll. - Bryr jag mig bara om det första promise-objektet som LYCKAS, och kan jag ignorera de som misslyckas?
AnvÀndPromise.any(). Detta Àr för scenarier som involverar redundans, som att prova flera Àndpunkter för samma resurs.
Avancerade mönster och övervÀganden frÄn verkligheten
Ăven om promise-kombinatorerna Ă€r otroligt kraftfulla, krĂ€ver professionell utveckling ofta lite mer nyans.
BegrÀnsning av samtidighet och strypning (throttling)
Vad hÀnder om du har en array med 1 000 ID:n och du vill hÀmta data för var och en? Om du naivt skickar alla 1 000 promise-genererande anrop till `Promise.all()`, kommer du omedelbart att avfyra 1 000 nÀtverksförfrÄgningar. Detta kan ha flera negativa konsekvenser:
- Serveröverbelastning: Du kan överbelasta servern du anropar, vilket leder till fel eller försÀmrad prestanda för alla anvÀndare.
- Rate Limiting (hastighetsbegrÀnsning): De flesta publika API:er har hastighetsbegrÀnsningar. Du kommer troligen att slÄ i taket och fÄ `429 Too Many Requests`-fel.
- Klientresurser: Klienten (webblÀsare eller server) kan ha svÄrt att hantera sÄ mÄnga öppna nÀtverksanslutningar samtidigt.
Lösningen Ă€r att begrĂ€nsa samtidigheten genom att bearbeta promises i batcher. Ăven om du kan skriva din egen logik för detta, hanterar mogna bibliotek som `p-limit` eller `async-pool` detta elegant. HĂ€r Ă€r ett konceptuellt exempel pĂ„ hur du kan nĂ€rma dig det manuellt:
async function processInBatches(items, batchSize, processingFn) {
let results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
console.log(`Processing batch starting at index ${i}...`);
const batchPromises = batch.map(processingFn);
const batchResults = await Promise.allSettled(batchPromises);
results = results.concat(batchResults);
}
return results;
}
// Example usage:
const userIds = Array.from({ length: 20 }, (_, i) => i + 1);
// We will process 20 users in batches of 5
processInBatches(userIds, 5, id => mockApiCall(`user_${id}`, Math.random() * 1000))
.then(allResults => {
console.log('\nBatch processing complete.');
const successful = allResults.filter(r => r.status === 'fulfilled').length;
const failed = allResults.filter(r => r.status === 'rejected').length;
console.log(`Total Results: ${allResults.length}, Successful: ${successful}, Failed: ${failed}`);
});
En notering om avbrytande (cancellation)
En lĂ„ngvarig utmaning med inbyggda Promises Ă€r att de inte kan avbrytas. NĂ€r du vĂ€l har skapat ett promise kommer det att köras till slutförande. Ăven om `Promise.race` kan hjĂ€lpa dig att ignorera ett lĂ„ngsamt resultat, fortsĂ€tter den underliggande operationen att förbruka resurser. För nĂ€tverksförfrĂ„gningar Ă€r den moderna lösningen `AbortController`-API:et, som lĂ„ter dig signalera till en `fetch`-förfrĂ„gan att den ska avbrytas. Att integrera `AbortController` med promise-kombinatorer kan ge ett robust sĂ€tt att hantera och stĂ€da upp lĂ„ngvariga samtidiga uppgifter.
Slutsats: FrÄn sekventiellt till samtidigt tÀnkande
Att bemÀstra asynkron JavaScript Àr en resa. Den börjar med att förstÄ den entrÄdiga hÀndelseloopen, fortsÀtter med att anvÀnda Promises och `async/await` för tydlighet, och kulminerar i att tÀnka samtidigt för att maximera prestanda. Att skifta frÄn ett sekventiellt `await`-tÀnkesÀtt till ett parallellt-först-synsÀtt Àr en av de mest effektfulla förÀndringarna en utvecklare kan göra för att förbÀttra applikationens responsivitet.
Genom att utnyttja de inbyggda promise-kombinatorerna Àr du rustad för att hantera en mÀngd olika verkliga scenarier med elegans och precision:
- AnvÀnd `Promise.all()` för kritiska allt-eller-inget-databeroenden.
- Lita pÄ `Promise.allSettled()` för att bygga motstÄndskraftiga grÀnssnitt med oberoende komponenter.
- AnvÀnd `Promise.race()` för att upprÀtthÄlla tidsgrÀnser och förhindra oÀndliga vÀntetider.
- VÀlj `Promise.any()` för att skapa snabba och feltoleranta system med redundanta datakÀllor.
NĂ€sta gĂ„ng du skriver flera `await`-uttryck i rad, pausa och frĂ„ga dig sjĂ€lv: "Ăr dessa operationer verkligen beroende av varandra?" Om svaret Ă€r nej, har du ett utmĂ€rkt tillfĂ€lle att refaktorera din kod för samtidighet. Börja initiera dina promises tillsammans, vĂ€lj rĂ€tt kombinator för din logik och se din applikations prestanda skjuta i höjden.