تعلم JavaScript Promises و Async/Await في 20 دقيقة: دليل عملي للمطورين
في عالم تطوير الويب الحديث، غالبًا ما تكون هناك عمليات تستغرق وقتًا طويلاً لإكمالها، مثل الاستعلام عن واجهة برمجية (API) أو جلب البيانات من خادم. هذه العمليات تتطلب منا التعامل مع البرمجة غير المتزامنة (Asynchronous Programming) لضمان بقاء واجهة المستخدم سريعة الاستجابة وتوفير تجربة سلسة للمستخدم. في JavaScript، يُعد مفهوم Promise حجر الزاوية في التعامل مع هذه العمليات، بينما يوفر بناء الجملة async/await طريقة أكثر وضوحًا وقابلية للقراءة لإدارة الـ Promises.
على الرغم من أهميتها، قد يكون فهم كيفية عمل الـ Promises واستخدامها بشكل فعال أمرًا معقدًا للمبتدئين. لذلك، بدلًا من الشرح النظري التقليدي، سنعتمد في هذا الدليل على التعلم بالممارسة. سنكمل أربع مهام عملية بحلول نهاية المقال، والتي ستساعدك على استيعاب هذه المفاهيم بشكل عميق:
- المهمة 1: شرح أساسيات الـ
Promiseباستخدام مثال عيد الميلاد. - المهمة 2: بناء لعبة تخمين الأرقام.
- المهمة 3: جلب معلومات الدول من واجهة برمجية (
API). - المهمة 4: جلب الدول المجاورة لدولة معينة.
ملاحظة:
إذا كنت ترغب في متابعة الأمثلة بشكل عملي، يمكنك تنزيل الموارد اللازمة من هذا الرابط: https://bit.ly/3m4bjWI
أساسيات الـ Promises: مثال عيد الميلاد
لتبسيط مفهوم الـ Promise، دعنا نتخيل سيناريو واقعيًا. صديقتي “كايو” وعدتني بصنع كعكة لعيد ميلادي بعد أسبوعين. إذا سارت الأمور على ما يرام ولم تمرض “كايو”، فسنحصل على عدد معين من الكعك. أما إذا مرضت، فلن نحصل على أي كعك. في كلتا الحالتين، سنقيم الحفل.
في هذه المهمة الأولى، سنقوم بتحويل هذه القصة إلى شيفرة برمجية.
إنشاء دالة تعيد Promise
لنبدأ بإنشاء دالة تُرجع كائن Promise:
const onMyBirthday = (isKayoSick) => { return new Promise((resolve, reject) => { setTimeout(() => { if (!isKayoSick) { resolve(2); } else { reject(new Error("I am sad")); } }, 2000); }); };
في JavaScript، يمكننا إنشاء Promise جديد باستخدام new Promise()، والذي يستقبل دالة كوسيط. هذه الدالة بدورها تستقبل وسيطين: resolve و reject، وهما دالتان استدعاء (callback functions) توفرهما JavaScript بشكل افتراضي.
دعنا نلقي نظرة فاحصة على الشيفرة أعلاه. عندما نقوم بتشغيل الدالة onMyBirthday، بعد 2000 مللي ثانية (أي ثانيتين):
- إذا لم تكن “كايو” مريضة (
!isKayoSick)، فإننا نستدعي الدالةresolveمع القيمة2كوسيط (عدد الكعكات). - إذا كانت “كايو” مريضة (
isKayoSick)، فإننا نستدعي الدالةrejectمع كائن خطأ (new Error("I am sad")). على الرغم من أنه يمكنك تمرير أي شيء إلىrejectكوسيط، يُوصى بشدة بتمرير كائنError.
استخدام then، catch، و finally
بما أن الدالة onMyBirthday() تُرجع كائن Promise، فإن لدينا إمكانية الوصول إلى ثلاث دوال مساعدة هامة: .then()، .catch()، و .finally(). هذه الدوال تسمح لنا بالتعامل مع نتيجة الـ Promise سواء كانت ناجحة أو فاشلة، بالإضافة إلى تنفيذ شيفرة معينة بغض النظر عن النتيجة.
دعنا نرى كيف تعمل هذه الدوال:
إذا لم تكن “كايو” مريضة (الـ Promise ينجح):
onMyBirthday(false) .then((result) => { console.log(`I have ${result} cakes`); // في الـ console: I have 2 cakes }) .catch((error) => { console.log(error); // لا يتم تشغيله }) .finally(() => { console.log("Party"); // يظهر في الـ console دائمًا: Party });
إذا كانت “كايو” مريضة (الـ Promise يفشل):
onMyBirthday(true) .then((result) => { console.log(`I have ${result} cakes`); // لا يتم تشغيله }) .catch((error) => { console.log(error); // في الـ console: Error: I am sad }) .finally(() => { console.log("Party"); // يظهر في الـ console دائمًا: Party });
الآن، نأمل أن تكون قد فهمت الفكرة الأساسية لكيفية عمل الـ Promise. دعنا ننتقل إلى المهمة الثانية.
بناء لعبة تخمين الأرقام باستخدام Promises و Async/Await
في هذه المهمة، سنقوم ببناء لعبة تخمين بسيطة. ستساعدنا هذه اللعبة على فهم كيفية ربط الـ Promises معًا وكيف يمكن لـ async/await تبسيط الشيفرة.
متطلبات اللعبة (User Stories)
إليك متطلبات اللعبة في شكل قصص مستخدم:
- يمكن للمستخدم إدخال رقم.
- يختار النظام رقمًا عشوائيًا من 1 إلى 6.
- إذا كان رقم المستخدم مساويًا للرقم العشوائي، يحصل المستخدم على نقطتين.
- إذا كان رقم المستخدم يختلف عن الرقم العشوائي بمقدار 1 (أكبر أو أصغر)، يحصل المستخدم على نقطة واحدة.
- بخلاف ذلك، يحصل المستخدم على 0 نقطة.
- يمكن للمستخدم لعب اللعبة طالما أراد ذلك.
إنشاء دالة enterNumber()
بالنسبة للقصص الأربعة الأولى، سنقوم بإنشاء دالة enterNumber تُرجع Promise:
const enterNumber = () => { return new Promise((resolve, reject) => { // سنبدأ من هنا }); };
أول شيء نحتاج إلى فعله هو طلب رقم من المستخدم واختيار رقم عشوائي بين 1 و 6:
const enterNumber = () => { return new Promise((resolve, reject) => { const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // اطلب من المستخدم إدخال رقم const randomNumber = Math.floor(Math.random() * 6 + 1); // اختر رقمًا عشوائيًا بين 1 و 6 }); };
الآن، قد يقوم المستخدم بإدخال قيمة ليست رقمًا. في هذه الحالة، دعنا نستدعي الدالة reject مع كائن خطأ:
const enterNumber = () => { return new Promise((resolve, reject) => { const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // اطلب من المستخدم إدخال رقم const randomNumber = Math.floor(Math.random() * 6 + 1); // اختر رقمًا عشوائيًا بين 1 و 6 if (isNaN(userNumber)) { reject(new Error("Wrong Input Type")); // إذا أدخل المستخدم قيمة ليست رقمًا، قم بتشغيل reject مع خطأ } }); };
الخطوة التالية هي التحقق مما إذا كان userNumber مساويًا لـ randomNumber. إذا كان كذلك، نريد أن نمنح المستخدم نقطتين، ويمكننا تشغيل الدالة resolve بتمرير كائن { points: 2, randomNumber }. لاحظ هنا أننا نريد أيضًا معرفة randomNumber عندما يتم حل الـ Promise.
إذا كان userNumber يختلف عن randomNumber بمقدار واحد، فإننا نمنح المستخدم نقطة واحدة. بخلاف ذلك، نمنحه 0 نقطة:
return new Promise((resolve, reject) => { const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // اطلب من المستخدم إدخال رقم const randomNumber = Math.floor(Math.random() * 6 + 1); // اختر رقمًا عشوائيًا بين 1 و 6 if (isNaN(userNumber)) { reject(new Error("Wrong Input Type")); // إذا أدخل المستخدم قيمة ليست رقمًا، قم بتشغيل reject مع خطأ } if (userNumber === randomNumber) { // إذا كان رقم المستخدم يطابق الرقم العشوائي، أرجع نقطتين resolve({ points: 2, randomNumber }); } else if (userNumber === randomNumber - 1 || userNumber === randomNumber + 1) { // إذا كان رقم المستخدم يختلف عن الرقم العشوائي بمقدار 1، أرجع نقطة واحدة resolve({ points: 1, randomNumber }); } else { // وإلا، أرجع 0 نقطة resolve({ points: 0, randomNumber }); } });
دالة متابعة اللعبة: continueGame()
دعنا أيضًا ننشئ دالة أخرى للسؤال عما إذا كان المستخدم يريد متابعة اللعبة:
const continueGame = () => { return new Promise((resolve) => { if (window.confirm("Do you want to continue?")) { // اسأل إذا كان المستخدم يريد متابعة اللعبة باستخدام نافذة تأكيد resolve(true); } else { resolve(false); } }); };
لاحظ هنا أننا أنشأنا Promise، لكنه لا يستخدم دالة الاستدعاء reject. وهذا أمر مقبول تمامًا.
إدارة التخمين بدالة handleGuess() باستخدام then/catch
الآن دعنا ننشئ دالة لإدارة عملية التخمين:
const handleGuess = () => { enterNumber() // هذه الدالة تُرجع Promise .then((result) => { alert(`Dice: ${result.randomNumber} : you got ${result.points} points`); // عندما يتم تشغيل resolve، نحصل على النقاط والرقم العشوائي // دعنا نسأل المستخدم إذا كان يريد متابعة اللعبة continueGame().then((result) => { if (result) { handleGuess(); // إذا نعم، نشغل handleGuess مرة أخرى } else { alert("Game ends"); // إذا لا، نعرض تنبيهًا } }); }) .catch((error) => alert(error)); }; handleGuess(); // تشغيل دالة handleGuess
هنا عندما نستدعي handleGuess، تُرجع enterNumber() الآن Promise:
- إذا تم حل الـ
Promise(resolved)، فإننا نستدعي الدالة.then()ونعرض رسالة تنبيه. كما نسأل إذا كان المستخدم يريد المتابعة. - إذا تم رفض الـ
Promise(rejected)، فإننا نعرض رسالة تنبيه تحتوي على الخطأ.
كما ترى، الشيفرة صعبة القراءة إلى حد ما بسبب تداخل الـ .then(). دعنا نعيد هيكلة الدالة handleGuess قليلًا باستخدام بناء الجملة async/await.
تحسين الشيفرة باستخدام Async/Await و Try/Catch
يُعد async/await طريقة حديثة وأكثر أناقة للتعامل مع الـ Promises، مما يجعل الشيفرة غير المتزامنة تبدو وكأنها شيفرة متزامنة:
const handleGuess = async () => { try { const result = await enterNumber(); // بدلاً من then، يمكننا الحصول على النتيجة مباشرة بوضع await قبل الـ Promise alert(`Dice: ${result.randomNumber} : you got ${result.points} points`); const isContinuing = await continueGame(); if (isContinuing) { handleGuess(); } else { alert("Game ends"); } } catch (error) { // بدلاً من catch، يمكننا استخدام بناء الجملة try, catch alert(error); } }; handleGuess(); // تشغيل دالة handleGuess
كما ترى، قمنا بإنشاء دالة async بوضع الكلمة المفتاحية async قبل الأقواس. ثم في الدالة الـ async:
- بدلاً من الدالة
.then()، يمكننا الحصول على النتائج مباشرة بمجرد وضع الكلمة المفتاحيةawaitقبل الـPromise. - بدلاً من الدالة
.catch()، يمكننا استخدام بناء الجملةtry...catchللتعامل مع الأخطاء.
الشيفرة الكاملة للعبة التخمين
إليك الشيفرة الكاملة لهذه المهمة مرة أخرى للرجوع إليها:
const enterNumber = () => { return new Promise((resolve, reject) => { const userNumber = Number(window.prompt("Enter a number (1 - 6):")); const randomNumber = Math.floor(Math.random() * 6 + 1); if (isNaN(userNumber)) { reject(new Error("Wrong Input Type")); } if (userNumber === randomNumber) { resolve({ points: 2, randomNumber }); } else if (userNumber === randomNumber - 1 || userNumber === randomNumber + 1) { resolve({ points: 1, randomNumber }); } else { resolve({ points: 0, randomNumber }); } }); }; const continueGame = () => { return new Promise((resolve) => { if (window.confirm("Do you want to continue?")) { resolve(true); } else { resolve(false); } }); }; const handleGuess = async () => { try { const result = await enterNumber(); alert(`Dice: ${result.randomNumber} : you got ${result.points} points`); const isContinuing = await continueGame(); if (isContinuing) { handleGuess(); } else { alert("Game ends"); } } catch (error) { alert(error); } }; handleGuess();
لقد انتهينا من المهمة الثانية. دعنا ننتقل إلى الثالثة.
جلب بيانات الدول من واجهة برمجية (API)
سترى أن الـ Promises تُستخدم بكثرة عند جلب البيانات من واجهة برمجية (API). إذا فتحت الرابط https://restcountries.eu/rest/v2/alpha/col في متصفح جديد، سترى بيانات الدولة بتنسيق JSON.
باستخدام Fetch API، يمكننا جلب البيانات عن طريق:
استخدام fetch() مع Async/Await
const fetchData = async () => { const res = await fetch("https://restcountries.eu/rest/v2/alpha/col"); // fetch() تُرجع Promise، لذا نحتاج إلى انتظارها const country = await res.json(); // res هي الآن مجرد استجابة HTTP، لذا نحتاج إلى استدعاء res.json() console.log(country); // سيتم تسجيل بيانات كولومبيا في الـ console }; fetchData();
الآن بعد أن أصبح لدينا بيانات الدولة التي نريدها، دعنا ننتقل إلى المهمة الأخيرة.
جلب بيانات الدول المجاورة باستخدام Promise.all
في هذه المهمة، سنقوم بتوسيع ما تعلمناه لجلب معلومات الدول المجاورة. لنفترض أن لدينا دالة fetchCountry جاهزة، والتي تجلب البيانات من نقطة النهاية (endpoint): https://restcountries.eu/rest/v2/alpha/${alpha3Code}، حيث alpha3Code هو رمز الدولة (مثل col لكولومبيا). كما أنها تتعامل مع أي أخطاء قد تحدث أثناء جلب البيانات.
دالة fetchCountry()
const fetchCountry = async (alpha3Code) => { try { const res = await fetch(`https://restcountries.eu/rest/v2/alpha/${alpha3Code}`); const data = await res.json(); return data; } catch (error) { console.log(error); } };
جلب بيانات كولومبيا
دعنا ننشئ دالة fetchCountryAndNeighbors ونجلب معلومات كولومبيا بتمرير "col" كـ alpha3Code:
const fetchCountryAndNeighbors = async () => { const columbia = await fetchCountry("col"); console.log(columbia); }; fetchCountryAndNeighbors();
الآن، إذا نظرت في الـ console، يمكنك رؤية كائن يبدو كالتالي:
في الكائن، توجد خاصية borders وهي قائمة برموز alpha3Code للدول المجاورة لكولومبيا.
جلب الدول المجاورة باستخدام Promise.all
إذا حاولنا الآن جلب الدول المجاورة باستخدام .map():
const neighbors = columbia.borders.map((border) => fetchCountry(border));
فإن المتغير neighbors سيكون مصفوفة من كائنات Promise. عند العمل مع مصفوفة من الـ Promises التي نريد حلها جميعًا بشكل متزامن، نحتاج إلى استخدام Promise.all():
const fetchCountryAndNeighbors = async () => { const columbia = await fetchCountry("col"); const neighbors = await Promise.all( columbia.borders.map((border) => fetchCountry(border)) ); console.log(neighbors); }; fetchCountryAndNeighbors();
في الـ console، يجب أن نتمكن من رؤية قائمة بكائنات الدول المجاورة.
الشيفرة الكاملة لجلب الدول المجاورة
إليك الشيفرة الكاملة للمهمة 4 للرجوع إليها:
const fetchCountry = async (alpha3Code) => { try { const res = await fetch(`https://restcountries.eu/rest/v2/alpha/${alpha3Code}`); const data = await res.json(); return data; } catch (error) { console.log(error); } }; const fetchCountryAndNeighbors = async () => { const columbia = await fetchCountry("col"); const neighbors = await Promise.all( columbia.borders.map((border) => fetchCountry(border)) ); console.log(neighbors); }; fetchCountryAndNeighbors();
الخلاصة التقنية
بعد إكمال هذه المهام الأربع، يتضح لنا أن الـ Promise و async/await أدوات لا غنى عنها في JavaScript الحديثة للتعامل مع العمليات غير المتزامنة. لقد رأينا كيف يمكن للـ Promises إدارة النتائج المحتملة (النجاح أو الفشل) للعمليات التي لا تحدث فورًا، وكيف أن .then() و .catch() و .finally() توفر طريقة منظمة للتعامل مع هذه النتائج.
الأهم من ذلك، أظهر لنا async/await كيف يمكننا كتابة شيفرة غير متزامنة تبدو وكأنها متزامنة، مما يحسن بشكل كبير من قابلية القراءة والصيانة، خاصة عند التعامل مع سلاسل طويلة من العمليات غير المتزامنة أو عند جلب البيانات من واجهات برمجية متعددة. استخدام try...catch مع async/await يوفر أيضًا آلية قوية وواضحة للتعامل مع الأخطاء. إن إتقان هذه المفاهيم يضع المطور على المسار الصحيح لبناء تطبيقات ويب قوية وفعالة وسريعة الاستجابة.