Hogyan készítsünk saját Promise-t JavaScriptben?

Ü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:

  1. 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.
  2. 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.
  3. 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 és reject.
  • Belső állapotkezelés (pending, fulfilled, rejected).
  • .then() metódus, amely onFulfilled és onRejected 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 és this.error: Itt tároljuk a végső eredményt vagy hibát.
  • this.onFulfilledCallbacks és this.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án queueMicrotask 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 vagy onRejected nincs megadva, default függvényekkel helyettesítjük őket. Az onFulfilled default egyszerűen átadja az értéket (value => value), míg az onRejected default továbbdobja a hibát (err => { throw err; }), így a hiba tovább terjedhet a láncban, amíg egy megfelelő .catch() vagy onRejected kezelő el nem kapja.
  • Új MyPromise: A .then() metódus mindig egy új MyPromise példányt ad vissza. Ennek a belső executor függvényének a resolve és reject 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 vagy onRejected 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 a onFulfilledCallbacks vagy onRejectedCallbacks tömbökben, hogy majd a resolve vagy reject hívása után fussanak le.
    • Ha fulfilled vagy rejected: A callbacket azonnal (aszinkron módon, a queueMicrotask-on keresztül) futtatjuk, mivel a Promise már megállapodott.

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

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük