الوعود في JavaScript: Promise و async و await
في درس المؤقتات اكتشفت أن JavaScript لا تنتظر: setTimeout تسجّل المهمة وتكمل. لكن ماذا عن عملية تحتاج نتيجتها، مثل جلب بيانات من الإنترنت قد تأخذ ثانيتين؟ لا يمكن أن تتجمد الصفحة حتى تصل البيانات، ولا يمكن أن تستخدم البيانات قبل وصولها. الحل الذي تبنته اللغة هو الوعد (Promise). وهذا الدرس مفتاح كل تعامل مع الخوادم.
ما هو الوعد؟
Section titled “ما هو الوعد؟”الوعد كائن يمثل نتيجة ستأتي لاحقاً.
تخيّل أنك طلبت وجبة في مطعم وأعطوك جهازاً صغيراً يرنّ. الجهاز ليس الوجبة، لكنه وعد بها. تجلس وتتحدث مع أصحابك، وعندما يرنّ تأخذ وجبتك. وقد يخبرونك بدلاً من ذلك أن الصنف انتهى.
لكل وعد ثلاث حالات:
| الحالة | معناها |
|---|---|
pending |
معلّق: العملية ما زالت جارية |
fulfilled |
تحقق: وصلت النتيجة |
rejected |
رُفض: حدث خطأ |
والوعد ينتقل من «معلّق» إلى إحدى الحالتين مرة واحدة فقط، ثم لا يتغير.
دالة تُرجع وعداً
Section titled “دالة تُرجع وعداً”سنكتب دالة تحاكي جلب بيانات مستخدم من خادم، وتأخذ نصف ثانية:
function fetchUser(id) { return new Promise((resolve, reject) => { setTimeout(() => { if (id > 0) { resolve({ id: id, name: "سارة" }); } else { reject(new Error("رقم المستخدم غير صحيح")); } }, 500); });}new Promiseتأخذ دالة لها معاملان:resolveوreject.- تستدعي
resolve(القيمة)عند النجاح، فيتحقق الوعد بهذه القيمة. - تستدعي
reject(الخطأ)عند الفشل، فيُرفض الوعد.
في الواقع قليلاً ما ستكتب new Promise بنفسك. أغلب الوقت ستستخدم دوالاً جاهزة تُرجع وعوداً، مثل fetch في الدرس القادم. لكن فهم ما بداخلها يزيل الغموض.
استقبال النتيجة: then و catch
Section titled “استقبال النتيجة: then و catch”console.log("1. أرسلنا الطلب");
fetchUser(7) .then((user) => { console.log("3. وصل المستخدم:", user.name); }) .catch((error) => { console.log("حدث خطأ:", error.message); });
console.log("2. البرنامج لم يتوقف");1. أرسلنا الطلب2. البرنامج لم يتوقف3. وصل المستخدم: سارةthen: تسجّل دالة تُنفَّذ عند تحقق الوعد، وتستقبل قيمته.catch: تسجّل دالة تُنفَّذ عند رفضه، وتستقبل الخطأ.
والترتيب كما في المؤقتات: السطر 2 طُبع قبل 3، لأن البرنامج أكمل طريقه ولم ينتظر.
وحالة الفشل:
fetchUser(-1) .then((user) => { console.log("وصل:", user.name); }) .catch((error) => { console.log("حدث خطأ:", error.message); });حدث خطأ: رقم المستخدم غير صحيحasync و await: كود يُقرأ بالترتيب
Section titled “async و await: كود يُقرأ بالترتيب”then تعمل، لكن عندما تتوالى عدة عمليات يتحول الكود إلى سلاسل طويلة صعبة القراءة. الصيغة الحديثة أوضح بكثير:
async function showUser() { console.log("جارٍ التحميل..."); const user = await fetchUser(7); console.log("أهلاً يا", user.name);}
showUser();جارٍ التحميل...أهلاً يا سارةasyncقبل الدالة: تعلن أن بداخلها انتظاراً.awaitقبل الوعد: «توقف هنا حتى يتحقق الوعد، ثم أعطني قيمته».
الكود يُقرأ من الأعلى إلى الأسفل كأنه كود عادي. والمهم: await توقف هذه الدالة فقط، أما باقي البرنامج والصفحة فيبقيان يعملان. لا شيء يتجمد.
معالجة الأخطاء مع await
Section titled “معالجة الأخطاء مع await”مع await نعود إلى try و catch التي تعلمتها في الدرس السابق:
async function loadUser(id) { try { const user = await fetchUser(id); console.log("تم التحميل:", user.name); } catch (error) { console.log("فشل التحميل:", error.message); } finally { console.log("انتهت المحاولة"); }}
loadUser(-5);فشل التحميل: رقم المستخدم غير صحيحانتهت المحاولةإذا رُفض الوعد، ترمي await الخطأ كأنه throw عادي، فتلتقطه catch.
دالة async تُرجع وعداً دائماً
Section titled “دالة async تُرجع وعداً دائماً”async function getName() { const user = await fetchUser(3); return user.name;}
const result = getName();console.log(result instanceof Promise);
getName().then((name) => console.log("الاسم:", name));trueالاسم: سارةحتى لو كتبت return لنص عادي، فإن دالة async تلفه في وعد. لذلك من يستدعيها يحتاج await أو then ليحصل على القيمة.
عدة عمليات معاً: Promise.all
Section titled “عدة عمليات معاً: Promise.all”إذا احتجت ثلاثة مستخدمين، يمكنك انتظارهم واحداً بعد الآخر:
async function sequential() { const start = Date.now(); await fetchUser(1); await fetchUser(2); await fetchUser(3); const seconds = (Date.now() - start) / 1000; console.log("بالتتابع:", seconds.toFixed(1), "ثانية");}
sequential();بالتتابع: 1.5 ثانيةكل عملية تنتظر انتهاء التي قبلها، مع أنها لا تعتمد عليها. الأسرع أن تبدأها كلها معاً:
async function parallel() { const start = Date.now(); const users = await Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]); const seconds = (Date.now() - start) / 1000; console.log("معاً:", seconds.toFixed(1), "ثانية"); console.log("العدد:", users.length);}
parallel();معاً: 0.5 ثانيةالعدد: 3Promise.all تأخذ مصفوفة وعود، وتُرجع وعداً واحداً يتحقق عندما تتحقق كلها، وقيمته مصفوفة النتائج بنفس الترتيب. وإذا رُفض أي واحد منها رُفض الكل.
تخيّل ثلاثة أصدقاء يطلبون وجباتهم في نفس اللحظة بدلاً من أن ينتظر كل واحد حتى يستلم الذي قبله.
جرّب بنفسك
Section titled “جرّب بنفسك”الناتج
الدالة wait أداة صغيرة مفيدة جداً: وعد يتحقق بعد مدة. احذف كلمة await من أمام wait(1000) وشاهد كيف يتغير ترتيب الناتج. ثم أضف throw new Error("انقطع الغاز") داخل makeTea.
أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان await: تحصل على الوعد نفسه بدلاً من قيمته، فيُطبع
Promiseبدلاً من البيانات، أو تكون الخصائصundefined. - نسيان معالجة الرفض: وعد مرفوض بلا
catchيسبب تحذير «Uncaught (in promise)». كلawaitخطرة تحتاجtryوcatch. - انتظار عمليات مستقلة بالتتابع: يضيع الوقت. استخدم
Promise.allعندما لا تعتمد العمليات على بعضها.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تفعل كلمة await؟
await تعلّق تنفيذ دالة async الحالية فقط، وباقي البرنامج والصفحة يبقيان يعملان.
سؤال
ثلاث عمليات مستقلة كل واحدة تأخذ ثانية. كم يأخذ await Promise.all لها تقريباً؟
Promise.all تبدأ العمليات معاً، فالزمن الكلي يساوي زمن أطول عملية، لا مجموع الأزمنة.
الخلاصة
Section titled “الخلاصة”- الوعد يمثل نتيجة ستأتي لاحقاً، وله ثلاث حالات: معلّق، ومتحقق، ومرفوض.
thenتستقبل النتيجة، وcatchتستقبل الخطأ.asyncوawaitتجعلان الكود غير المتزامن يُقرأ بالترتيب.- مع
awaitاستخدمtryوcatchلمعالجة الفشل. Promise.allتشغّل عدة عمليات مستقلة معاً وتنتظرها كلها.