تخطَّ إلى المحتوى

الوعود في JavaScript: Promise و async و await

الدرس 23 من 28 · JavaScript

في درس المؤقتات اكتشفت أن JavaScript لا تنتظر: setTimeout تسجّل المهمة وتكمل. لكن ماذا عن عملية تحتاج نتيجتها، مثل جلب بيانات من الإنترنت قد تأخذ ثانيتين؟ لا يمكن أن تتجمد الصفحة حتى تصل البيانات، ولا يمكن أن تستخدم البيانات قبل وصولها. الحل الذي تبنته اللغة هو الوعد (Promise). وهذا الدرس مفتاح كل تعامل مع الخوادم.

الوعد كائن يمثل نتيجة ستأتي لاحقاً.

تخيّل أنك طلبت وجبة في مطعم وأعطوك جهازاً صغيراً يرنّ. الجهاز ليس الوجبة، لكنه وعد بها. تجلس وتتحدث مع أصحابك، وعندما يرنّ تأخذ وجبتك. وقد يخبرونك بدلاً من ذلك أن الصنف انتهى.

لكل وعد ثلاث حالات:

الحالة معناها
pending معلّق: العملية ما زالت جارية
fulfilled تحقق: وصلت النتيجة
rejected رُفض: حدث خطأ

والوعد ينتقل من «معلّق» إلى إحدى الحالتين مرة واحدة فقط، ثم لا يتغير.

سنكتب دالة تحاكي جلب بيانات مستخدم من خادم، وتأخذ نصف ثانية:

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 نعود إلى 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 ليحصل على القيمة.

إذا احتجت ثلاثة مستخدمين، يمكنك انتظارهم واحداً بعد الآخر:

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 ثانية
العدد: 3

Promise.all تأخذ مصفوفة وعود، وتُرجع وعداً واحداً يتحقق عندما تتحقق كلها، وقيمته مصفوفة النتائج بنفس الترتيب. وإذا رُفض أي واحد منها رُفض الكل.

تخيّل ثلاثة أصدقاء يطلبون وجباتهم في نفس اللحظة بدلاً من أن ينتظر كل واحد حتى يستلم الذي قبله.

جرّب بنفسكJavaScript
أو اضغط Ctrl + Enter

الناتج

الدالة wait أداة صغيرة مفيدة جداً: وعد يتحقق بعد مدة. احذف كلمة await من أمام wait(1000) وشاهد كيف يتغير ترتيب الناتج. ثم أضف throw new Error("انقطع الغاز") داخل makeTea.

  • نسيان await: تحصل على الوعد نفسه بدلاً من قيمته، فيُطبع Promise بدلاً من البيانات، أو تكون الخصائص undefined.
  • نسيان معالجة الرفض: وعد مرفوض بلا catch يسبب تحذير «Uncaught (in promise)». كل await خطرة تحتاج try و catch.
  • انتظار عمليات مستقلة بالتتابع: يضيع الوقت. استخدم Promise.all عندما لا تعتمد العمليات على بعضها.

سؤال

ماذا تفعل كلمة await؟

سؤال

ثلاث عمليات مستقلة كل واحدة تأخذ ثانية. كم يأخذ await Promise.all لها تقريباً؟

  • الوعد يمثل نتيجة ستأتي لاحقاً، وله ثلاث حالات: معلّق، ومتحقق، ومرفوض.
  • then تستقبل النتيجة، و catch تستقبل الخطأ.
  • async و await تجعلان الكود غير المتزامن يُقرأ بالترتيب.
  • مع await استخدم try و catch لمعالجة الفشل.
  • Promise.all تشغّل عدة عمليات مستقلة معاً وتنتظرها كلها.