Üdvözöllek a JavaScript aszinkron programozás izgalmas világában! Ha valaha is dolgoztál modern webes alkalmazásokkal, szinte biztos, hogy találkoztál már a Promise-okkal. Ezek a speciális objektumok forradalmasították az aszinkron műveletek kezelését, búcsút intve a rettegett „Callback Hell”-nek és tisztább, olvashatóbb kódot eredményezve. De vajon elgondolkodtál már azon, hogyan is működnek a színfalak mögött? Mi történik, amikor meghívod a new Promise()
konstruktort? Ebből a cikkből nemcsak a Promise-ok alapjait sajátíthatod el mélyebben, hanem lépésről lépésre megalkotjuk a saját, egyszerűsített Promise implementációnkat JavaScriptben. Készülj fel, hogy mélyebbre merülj a JavaScript motorházteteje alá, és igazi mesterévé válj az aszinkron kódelrendezésnek!
Miért van szükségünk Promise-okra? Az Aszinkron Világ Kihívásai
A JavaScript alapvetően egy egy szálas nyelv, ami azt jelenti, hogy egyszerre csak egy műveletet képes végrehajtani. Ez egy böngésző környezetben létfontosságú, hiszen nem szeretnénk, ha egy hosszú adatbetöltés lefagyasztaná a teljes felhasználói felületet. Éppen ezért van szükség az aszinkron műveletekre. Amikor például egy API-hívást indítunk (fetch()
), egy fájlt olvasunk be, vagy egy időzítőt állítunk be (setTimeout()
), ezek a műveletek nem azonnal adják vissza az eredményt. A JavaScript motor ezeket háttérbe helyezi, és amint az eredmény elkészült, egy callback függvény segítségével értesít minket.
Kezdetben a callback függvények voltak az aszinkronitás királyai. Nézzünk egy egyszerű példát:
function fetchData(callback) {
setTimeout(() => {
const data = "Ezek az adatok.";
callback(data);
}, 1000);
}
fetchData(function(data) {
console.log(data); // "Ezek az adatok."
});
Ez még rendben van. De mi történik, ha több aszinkron műveletet szeretnénk egymás után végrehajtani, ahol az egyik eredménye a következő bemenete? Akkor alakul ki a hírhedt Callback Hell, vagy más néven a „pyramid of doom”:
function step1(callback) {
setTimeout(() => { console.log('1. lépés kész'); callback(); }, 500);
}
function step2(callback) {
setTimeout(() => { console.log('2. lépés kész'); callback(); }, 700);
}
function step3(callback) {
setTimeout(() => { console.log('3. lépés kész'); callback(); }, 300);
}
step1(() => {
step2(() => {
step3(() => {
console.log('Minden lépés kész!');
});
});
});
Ez a kód gyorsan olvashatatlanná és nehezen karbantarthatóvá válik, különösen hibakezeléssel kiegészítve. Itt jönnek a képbe a Promise-ok, amelyek elegánsabb és strukturáltabb megoldást kínálnak az aszinkron műveletek láncolására és kezelésére.
A Promise Alapfogalmak: Állapotok és Érték
A Promise egy olyan objektum, amely egy aszinkron művelet végső befejezését (vagy sikertelen befejezését) és annak eredményértékét képviseli. Három lehetséges állapota van:
- Pending (függőben): Az inicializált állapot, a Promise még nem teljesült és nem is utasíttatott el. Az aszinkron művelet még folyamatban van.
- Fulfilled (teljesült): A művelet sikeresen befejeződött, és a Promise egy értéket adott vissza. Ezt gyakran „resolved”-nak is nevezzük.
- Rejected (elutasítva): A művelet sikertelenül fejeződött be, és a Promise egy okot (általában egy hibát) adott vissza.
Fontos megjegyezni, hogy egy Promise csak egyszer állapodhat meg (settle). Azaz, amint fulfilled
vagy rejected
állapotba kerül, állapota és értéke már nem változhat meg. Ezt az állapotot „settled”-nek nevezzük.
Célunk: Egy Egyszerű MyPromise Létrehozása
A célunk, hogy létrehozzunk egy saját, egyszerűsített MyPromise
osztályt, amely legalább a következő alapvető funkciókat biztosítja:
- Konstruktor, amely egy úgynevezett „executor” függvényt fogad el.
- Az executor függvény két argumentumot kap:
resolve
ésreject
. - Belső állapotkezelés (
pending
,fulfilled
,rejected
). .then()
metódus, amelyonFulfilled
ésonRejected
callbackeket fogad el.- A
.then()
metódusnak láncolhatónak kell lennie, és új Promise-t kell visszaadnia.
Ahhoz, hogy megértsük a Promise-ok működését, figyelembe kell vennünk a JavaScript Event Loop működését és a Microtask Queue szerepét is. A Promise callbackjei (a .then()
metódusnak átadott függvények) nem azonnal, hanem a jelenlegi kódblokk futása után, de még a következő Event Loop ciklus előtt futnak le. Ez a feladatok ütemezésének kulcsfontosságú eleme, és mi is emulálni fogjuk a queueMicrotask()
(vagy régebbi böngészőkben a setTimeout(..., 0)
) használatával.
Az Alap Váz: MyPromise Osztály
Kezdjük a MyPromise
osztály alapvető struktúrájával. Szükségünk lesz belső változókra az állapot (status
), az érték (value
) és a hiba (error
) tárolására. Emellett gyűjtenünk kell azokat a callback függvényeket is, amelyeket a .then()
metódusokon keresztül adnak át, mivel előfordulhat, hogy a Promise még pending
állapotban van, amikor a .then()
-t meghívják.
class MyPromise {
constructor(executor) {
this.status = 'pending'; // Kezdeti állapot
this.value = undefined; // A Promise értéke, ha teljesül
this.error = undefined; // A Promise hibája, ha elutasítják
this.onFulfilledCallbacks = []; // Tömb a sikeres callbackek tárolására
this.onRejectedCallbacks = []; // Tömb a sikertelen callbackek tárolására
const resolve = (value) => {
// Egy Promise csak egyszer állapodhat meg
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
// Amint teljesült, futtassuk az összes tárolt onFulfilled callbacket
this.onFulfilledCallbacks.forEach(callback => {
queueMicrotask(() => callback(this.value)); // Aszinkron futtatás
});
}
};
const reject = (error) => {
// Egy Promise csak egyszer állapodhat meg
if (this.status === 'pending') {
this.status = 'rejected';
this.error = error;
// Amint elutasították, futtassuk az összes tárolt onRejected callbacket
this.onRejectedCallbacks.forEach(callback => {
queueMicrotask(() => callback(this.error)); // Aszinkron futtatás
});
}
};
try {
// Az executor függvényt azonnal meghívjuk
// Ebbe az executorba juttatja el a Promise a resolve és reject függvényeket
executor(resolve, reject);
} catch (err) {
// Ha az executor függvény hibát dob, a Promise-nak el kell utasítódnia
reject(err);
}
}
// A .then() metódus implementálása következik
// ...
}
Nézzük meg alaposabban, mi történik itt:
this.status
: Kezdetben'pending'
.this.value
ésthis.error
: Itt tároljuk a végső eredményt vagy hibát.this.onFulfilledCallbacks
ésthis.onRejectedCallbacks
: Ezek tömbök, mert előfordulhat, hogy több.then()
hívás is történik, mielőtt a Promise lefutna. Minden ilyen hívás regisztrál egy callbacket, amit később futtatni kell.resolve(value)
: Ez a belső függvény állítja be a Promise állapotát'fulfilled'
-re, és tárolja az eredményt. EzutánqueueMicrotask
segítségével aszinkron módon futtatja az összes regisztrált sikeres callbacket.reject(error)
: Hasonlóan, ez állítja be az állapotot'rejected'
-re, és tárolja a hibát, majd aszinkron futtatja a regisztrált hibakezelő callbackeket.- A
try...catch
blokk az executor körül kulcsfontosságú. Ha az executor kódja szinkron módon hibát dob, akkor a Promise-nak el kell utasítódnia ezzel a hibával.
A `.then()` Metódus Implementálása: A Láncolhatóság Kulcsa
A .then()
metódus a Promise-ok lelke, ez teszi lehetővé a láncolást és a Callback Hell elkerülését. Két opcionális argumentumot fogad el: egy onFulfilled
és egy onRejected
callbacket. A legfontosabb, hogy a .then()
**mindig egy új Promise-t ad vissza**, ami lehetővé teszi a további láncolást.
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.error = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => {
queueMicrotask(() => callback(this.value));
});
}
};
const reject = (error) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.error = error;
this.onRejectedCallbacks.forEach(callback => {
queueMicrotask(() => callback(this.error));
});
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// Ha a callbackek nem függvények, alakítsuk át őket alapértelmezett viselkedéssé
// Így a lánc tovább tud futni, ha pl. nincs megadva hibakezelő
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
const newPromise = new MyPromise((resolve, reject) => {
// Segédfüggvény, ami kezeli a callbackek futását
const handleCallback = (callback, data) => {
queueMicrotask(() => { // Mindig aszinkron futtatás
try {
const result = callback(data);
if (result instanceof MyPromise) {
// Ha a callback egy Promise-t ad vissza, arra várnunk kell
result.then(resolve, reject);
} else {
// Egyébként az eredményt adjuk tovább a következő Promise-nak
resolve(result);
}
} catch (err) {
// Ha a callback hibát dob, az elutasítja a következő Promise-t
reject(err);
}
});
};
if (this.status === 'pending') {
// Ha a Promise még függőben van, tároljuk a callbackeket
this.onFulfilledCallbacks.push(value => handleCallback(onFulfilled, value));
this.onRejectedCallbacks.push(error => handleCallback(onRejected, error));
} else if (this.status === 'fulfilled') {
// Ha már teljesült, azonnal futtassuk az onFulfilled callbacket
handleCallback(onFulfilled, this.value);
} else if (this.status === 'rejected') {
// Ha már elutasították, azonnal futtassuk az onRejected callbacket
handleCallback(onRejected, this.error);
}
});
return newPromise; // Mindig új Promise-t ad vissza a láncolhatóságért
}
// A .catch() metódus implementálása következik
// ...
}
Ez a rész a legösszetettebb, bontsuk ki:
- Default callbackek: Ha
onFulfilled
vagyonRejected
nincs megadva, default függvényekkel helyettesítjük őket. AzonFulfilled
default egyszerűen átadja az értéket (value => value
), míg azonRejected
default továbbdobja a hibát (err => { throw err; }
), így a hiba tovább terjedhet a láncban, amíg egy megfelelő.catch()
vagyonRejected
kezelő el nem kapja. - Új MyPromise: A
.then()
metódus mindig egy újMyPromise
példányt ad vissza. Ennek a belső executor függvényének aresolve
ésreject
metódusait hívogatjuk attól függően, hogy az aktuális callback mit ad vissza. handleCallback
: Ez a segédfüggvény burkolja be a callbackek futtatását.queueMicrotask()
: Fontos, hogy a callbackek mindig aszinkron módon futnak, még akkor is, ha a Promise már lefutott. Ez biztosítja a konzisztens aszinkron viselkedést és megakadályozza a „synchronous callback” problémákat.try...catch
: A callback függvények futása közben fellépő hibákat el kell kapni, és azzal el kell utasítani a *következő* Promise-t a láncban.- Promise-t ad vissza? Ha egy
onFulfilled
vagyonRejected
callback maga is egy Promise-t ad vissza, akkor a láncnak meg kell várnia annak a Promise-nak a teljesülését, és az eredményét vagy hibáját kell továbbadnia. Ez a Promise láncolás lényege! - Egyéb érték: Ha a callback egy nem-Promise értéket ad vissza, akkor azzal az értékkel teljesítjük a *következő* Promise-t.
- Állapotfüggő regisztráció/futtatás:
- Ha
pending
: A callbackeket egyszerűen eltároljuk aonFulfilledCallbacks
vagyonRejectedCallbacks
tömbökben, hogy majd aresolve
vagyreject
hívása után fussanak le. - Ha
fulfilled
vagyrejected
: A callbacket azonnal (aszinkron módon, aqueueMicrotask
-on keresztül) futtatjuk, mivel a Promise már megállapodott.
- Ha
A `.catch()` Metódus: Egyszerűbb Hibakezelés
A beépített JavaScript Promise-ok rendelkeznek egy .catch()
metódussal, ami tulajdonképpen egy szintaktikai cukorka a .then(null, onRejected)
hívásra. Mi is könnyedén implementálhatjuk:
class MyPromise {
// ... (korábbi kód, konstruktor és then metódus)
catch(onRejected) {
return this.then(null, onRejected);
}
}
Ez lehetővé teszi, hogy elegánsan kezeljük a hibákat a láncban, anélkül, hogy minden .then()
hívásnál megadnánk egy onRejected
callbacket.
Példa a Saját MyPromise Használatára
Most, hogy megvan a MyPromise
implementációnk, nézzünk néhány példát a használatára:
function asyncOperation(value, shouldReject = false) {
return new MyPromise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error(`Hiba történt: ${value}`));
} else {
resolve(`Sikeresen feldolgozva: ${value}`);
}
}, 500);
});
}
// Egyszerű láncolás
asyncOperation(1)
.then(result => {
console.log(result); // Sikeresen feldolgozva: 1
return asyncOperation(2); // Visszaadunk egy új Promise-t
})
.then(result => {
console.log(result); // Sikeresen feldolgozva: 2
return "Sikeres befejezés!"; // Visszaadunk egy sima értéket
})
.then(finalResult => {
console.log(finalResult); // Sikeres befejezés!
})
.catch(error => {
console.error("Valami hiba történt a láncban:", error);
});
// Hibakezelés
asyncOperation(3, true) // Ez a Promise elutasításra kerül
.then(result => {
console.log("Ez nem fog lefutni:", result);
})
.catch(error => {
console.error("Hiba elkapva:", error.message); // Hiba elkapva: Hiba történt: 3
return "Hiba sikeresen kezelve, továbbmehetünk"; // Visszaadunk egy értéket a catch-ből
})
.then(message => {
console.log(message); // Hiba sikeresen kezelve, továbbmehetünk
});
// Executorban dobott hiba
new MyPromise((resolve, reject) => {
throw new Error("Hiba az executorban!");
})
.catch(error => {
console.error("Executor hiba elkapva:", error.message); // Executor hiba elkapva: Hiba az executorban!
});
Ahogy láthatjuk, a saját MyPromise
implementációnk képes kezelni a Promise láncolást, az értékek és hibák továbbadását, valamint az aszinkron végrehajtást, nagyon hasonlóan a beépített Promise
-hoz. A queueMicrotask
használata biztosítja, hogy a .then()
callbackjei a mikrofeladat-sorba kerüljenek, így a jelenlegi végrehajtási blokk befejeződése után, de még a következő eseményhurok iteráció előtt lefutnak.
További Fejlesztési Lehetőségek és Korlátok
A mi MyPromise
implementációnk egy egyszerűsített verzió. A beépített Promise
sokkal robusztusabb és több funkcióval rendelkezik, mint például:
Promise.all()
,Promise.race()
,Promise.allSettled()
,Promise.any()
statikus metódusok..finally()
metódus, amely állapotfüggetlenül fut le.- Részletesebb hibakezelés (unhandled rejections).
- Optimalizált teljesítmény és memóriakezelés.
A saját Promise megírásának fő célja nem a beépített helyettesítése, hanem a mögöttes mechanizmusok mélyebb megértése. Ez az alapvető tudás felvértez téged azzal a képességgel, hogy hatékonyabban debuggolj, és jobban kihasználd a Promise-ok erejét a mindennapi fejlesztés során.
Összefoglalás
Gratulálok! Most már nemcsak tudod, hogyan kell használni a JavaScript Promise-okat, hanem mélyen beleláttál a működésükbe is. Megtudtuk, miért volt rájuk szükség a Callback Hell elkerülése végett, és hogyan kezelik az aszinkron műveletek különböző állapotait. Létrehoztunk egy saját MyPromise
osztályt, megvalósítva a resolve
és reject
mechanizmusokat, valamint a kulcsfontosságú .then()
metódust, amely lehetővé teszi a láncolást és az értékek, illetve hibák aszinkron továbbadását. A queueMicrotask
használatával pedig biztosítottuk a megfelelő végrehajtási sorrendet a JavaScript Event Loop kontextusában.
Ez a gyakorlat nemcsak a JavaScript belső működésébe engedett betekintést, hanem abba is, hogy hogyan épülnek fel az összetett aszinkron minták az alapvető programozási elvekből. A Promise-ok megértése elengedhetetlen a modern JavaScript fejlesztéshez, és reméljük, ez a cikk segített abban, hogy magabiztosabban használd őket a jövőbeli projektjeidben!
Ne feledd, a JavaScript tele van ilyen elegáns megoldásokkal, és a motorháztető alá nézve mindig újabb rétegeket fedezhetsz fel. Boldog kódolást!
Leave a Reply