دليل JavaScript Async/Await: تعلّم Callbacks وPromises وAsync/Await عبر مثال متجر المثلجات
إذا كنت تريد بناء تطبيقات أكثر كفاءة في JavaScript، ففهم البرمجة غير المتزامنة يعد خطوة أساسية. في هذا الدليل العملي سنشرح مفاهيم Callbacks وPromises وAsync/Await بطريقة سهلة، وذلك عبر مثال ممتع يحاكي تشغيل متجر لبيع المثلجات.
الهدف هنا ليس مجرد عرض تعريفات نظرية، بل تحويل المفاهيم إلى خطوات عملية تساعدك على فهم آلية تنفيذ المهام داخل JavaScript، وكيفية التعامل مع العمليات التي تحتاج وقتًا مثل جلب البيانات أو انتظار استجابة من المستخدم أو تنفيذ مهام مؤجلة.

ما المقصود بالبرمجة غير المتزامنة في JavaScript؟

البرمجة غير المتزامنة أو Asynchronous JavaScript هي أسلوب يتيح تقسيم العمليات الكبيرة إلى مهام أصغر، ثم تنفيذها بطريقة ذكية دون تعطيل بقية أجزاء التطبيق. هذا مهم جدًا عندما تتعامل مع مهام تحتاج وقتًا، مثل:
- قراءة البيانات من واجهة برمجية
API. - رفع الملفات.
- الانتظار لعدة ثوانٍ قبل تنفيذ مهمة.
- التعامل مع إدخال المستخدم.
بدلًا من إيقاف التطبيق بالكامل إلى أن تنتهي مهمة واحدة، تتيح البرمجة غير المتزامنة استمرار بقية الأعمال بالتوازي أو وفق ترتيب زمني مدروس.
الفرق بين التنفيذ المتزامن وغير المتزامن

ما هو النظام المتزامن؟
في النظام المتزامن Synchronous تُنفَّذ المهام واحدة تلو الأخرى. لا تبدأ المهمة التالية قبل انتهاء السابقة. تخيل أن لديك يدًا واحدة فقط وعليك إنجاز عشر مهام؛ ستضطر إلى تنفيذها بالتتابع.

لغة JavaScript بطبيعتها تعمل بأسلوب متزامن وضمن خيط تنفيذ واحد single-threaded، لكن هذا لا يمنعها من التعامل مع العمليات غير المتزامنة بآليات خاصة.
ما هو النظام غير المتزامن؟
في النظام غير المتزامن Asynchronous يمكن لبعض المهام أن تُدار باستقلالية دون أن تعطل بقية المهام. تخيل أن لديك عشر أيدٍ بدلًا من يد واحدة، فيمكن تنفيذ عدة أعمال في الوقت نفسه أو على الأقل دون انتظار خطّي صارم.

في هذا النموذج، كل مهمة تسير وفق مدتها الخاصة، ولا تُجبِر بقية المهام على التوقف.
تلخيص سريع للفارق


- في النظام المتزامن: تعطل مهمة واحدة قد يؤخر ما بعدها.
- في النظام غير المتزامن: كل مهمة تتابع مسارها حسب ظروفها الخاصة.
أمثلة عملية على الكود المتزامن وغير المتزامن

مثال على كود متزامن

هذا المثال يوضح تنفيذ الأوامر سطرًا بعد سطر:
console.log("I");
console.log("eat");
console.log("Ice Cream");

مثال على كود غير متزامن

هنا سنستخدم الدالة setTimeout() لتأخير تنفيذ جزء من الكود لمدة ثانيتين:
console.log("I");
setTimeout(() => {
console.log("eat");
}, 2000);
console.log("Ice Cream");
لاحظ أن السطر الأخير سيظهر قبل الرسالة المؤجلة، لأن JavaScript لا تتوقف عن تنفيذ بقية الأوامر أثناء الانتظار.

إعداد مشروع متجر المثلجات

يمكنك تنفيذ الأمثلة في CodePen أو VS Code أو أي محرر تفضله. يكفي أن تفتح ملف JavaScript وتعرض النتائج في وحدة التحكم Console.
ما هي Callbacks في JavaScript؟

عندما تمرر دالة إلى دالة أخرى على أنها وسيط argument، فهذه تسمى callback. الفكرة ببساطة هي: نفّذ هذا الجزء من الكود لاحقًا عندما يحين الوقت المناسب.

لماذا نستخدم Callbacks؟
عندما تكون لدينا مهمة معقدة، فإننا نقسمها إلى خطوات أصغر. ومن أجل ضمان ترتيب هذه الخطوات زمنيًا ومنطقيًا، نستخدم callbacks.
في مثال تحضير المثلجات، لا يمكن تقديم المنتج قبل تقطيع الفاكهة أو إضافة المكونات أو تشغيل الآلة. لذلك نحتاج إلى ربط كل خطوة بما بعدها.

تقسيم المشروع إلى مكونات

- المخزن: يحتوي على المكونات، ويمثل جانب البيانات أو
Backend. - المطبخ: تجري فيه عملية الإنتاج، ويمكن تشبيهه بالمنطق التنفيذي أو الواجهة العملية.
تخزين البيانات داخل كائن

let stocks = {
Fruits: ["strawberry", "grapes", "banana", "apple"],
liquid: ["water", "ice"],
holder: ["cone", "cup", "stick"],
toppings: ["chocolate", "peanuts"],
};
إنشاء دالتين: الطلب والإنتاج

let order = () => {};
let production = () => {};
سنربط الدالتين باستخدام callback:
let order = (call_production) => {
call_production();
};
let production = () => {};
اختبار بسيط لفهم العلاقة
let order = (call_production) => {
console.log("Order placed. Please call production");
call_production();
};
let production = () => {
console.log("Production has started");
};
order(production);

إضافة اسم الفاكهة إلى الطلب
let order = (fruit_name, call_production) => {
call_production();
};
let production = () => {};
order("", production);
الآن سنضيف عامل الزمن باستخدام setTimeout() لأن كل خطوة في المشروع تستغرق مدة مختلفة.

let order = (fruit_name, call_production) => {
setTimeout(function () {
console.log(`${stocks.Fruits[fruit_name]} was selected`);
call_production();
}, 2000);
};
let production = () => {};
order(0, production);

كتابة منطق الإنتاج باستخدام تداخل setTimeout()
let production = () => {
setTimeout(() => {
console.log("production has started");
setTimeout(() => {
console.log("The fruit has been chopped");
setTimeout(() => {
console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} Added`);
setTimeout(() => {
console.log("start the machine");
setTimeout(() => {
console.log(`Ice cream placed on ${stocks.holder[1]}`);
setTimeout(() => {
console.log(`${stocks.toppings[0]} as toppings`);
setTimeout(() => {
console.log("serve Ice cream");
}, 2000);
}, 3000);
}, 2000);
}, 1000);
}, 1000);
}, 2000);
}, 0);
};

مشكلة Callback Hell
هذا التداخل الكبير في الدوال يجعل الكود أصعب في القراءة والصيانة. هذه المشكلة تعرف باسم Callback Hell.

كيف تساعد Promises في حل مشكلة Callback Hell؟

تم تصميم Promises لتقديم طريقة أنظف في إدارة العمليات غير المتزامنة. بدلًا من تداخل الدوال داخل بعضها، يمكننا ربط المهام على شكل سلسلة واضحة.


حالات Promise
Pending: الحالة الابتدائية، والوعد ما زال قيد التنفيذ.Resolved: تم تنفيذ المهمة بنجاح.Rejected: فشلت المهمة أو تعذر إكمالها.
بناء دالة order() باستخدام Promise
let is_shop_open = true;
let order = (time, work) => {
return new Promise((resolve, reject) => {
if (is_shop_open) {
setTimeout(() => {
resolve(work());
}, time);
} else {
reject(console.log("Our shop is closed"));
}
});
};
بهذا الشكل أصبحت لدينا دالة مرنة تستقبل الزمن والعمل المطلوب تنفيذه، ثم تعيد Promise يمكن مواصلة التعامل معها لاحقًا.
اختبار أولي
order(2000, () => console.log(`${stocks.Fruits[0]} was selected`));

ربط الوعود باستخدام .then()
يمكننا تحديد ما يجب أن يحدث بعد كل خطوة ناجحة باستخدام المعالج .then(). هذا الأسلوب يسمى Promise Chaining.

من المهم إعادة return داخل كل .then() حتى تستمر السلسلة بشكل صحيح.
order(2000, () => console.log(`${stocks.Fruits[0]} was selected`))
.then(() => {
return order(0, () => console.log("production has started"));
})
.then(() => {
return order(2000, () => console.log("Fruit has been chopped"));
})
.then(() => {
return order(1000, () => console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`));
})
.then(() => {
return order(1000, () => console.log("start the machine"));
})
.then(() => {
return order(2000, () => console.log(`ice cream placed on ${stocks.holder[1]}`));
})
.then(() => {
return order(3000, () => console.log(`${stocks.toppings[0]} as toppings`));
})
.then(() => {
return order(2000, () => console.log("Serve Ice Cream"));
});

معالجة الأخطاء باستخدام .catch()
عند فشل أي خطوة في السلسلة، نستخدم .catch() لالتقاط الخطأ.
let is_shop_open = false;
.catch(() => {
console.log("Customer left");
})

تنفيذ كود نهائي باستخدام .finally()
المعالج .finally() يُنفذ سواء نجحت العملية أو فشلت، وهو مثالي للخطوات الختامية.
.finally(() => {
console.log("end of day");
})

كيف يعمل Async/Await في JavaScript؟

يعد Async/Await طريقة أكثر وضوحًا لكتابة الكود غير المتزامن المعتمد على Promises. وهو لا يلغي الوعود، بل يبني فوقها واجهة كتابة أسهل قراءة وصيانة.
تحويل الدالة إلى دالة غير متزامنة
عند وضع الكلمة المفتاحية async قبل الدالة، فإنها تصبح دالة تعيد Promise تلقائيًا.
function order() {
return new Promise((resolve, reject) => {
// Write code here
});
}
async function order() {
// Write code here
}
استخدام try وcatch
مع Async/Await نعتمد غالبًا على try لتنفيذ الكود وcatch لالتقاط الأخطاء، ويمكن أيضًا استخدام finally.
async function kitchen() {
try {
await abc;
} catch (error) {
console.log("abc does not exist", error);
} finally {
console.log("Runs code anyways");
}
}
kitchen();
ما وظيفة await؟
تجعل الكلمة المفتاحية await تنفيذ الدالة الحالية ينتظر حتى تكتمل Promise، ثم تُعاد النتيجة. لكنها لا توقف التطبيق بالكامل، بل توقف فقط سياق التنفيذ داخل تلك الدالة غير المتزامنة.
مثال بسيط على await
function toppings_choice() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(console.log("which topping would you love?"));
}, 3000);
});
}
async function kitchen() {
console.log("A");
console.log("B");
console.log("C");
await toppings_choice();
console.log("D");
console.log("E");
}
kitchen();
console.log("doing the dishes");
console.log("cleaning the tables");
console.log("taking orders");

الفكرة هنا أن المطبخ ينتظر قرار العميل حول الإضافة المناسبة، لكن بقية الأعمال خارج هذا السياق تظل مستمرة.
بناء متجر المثلجات بالكامل باستخدام Async/Await

سننشئ دالة مساعدة لإدارة الوقت، ثم نستخدمها داخل دالة kitchen().
let is_shop_open = true;
function time(ms) {
return new Promise((resolve, reject) => {
if (is_shop_open) {
setTimeout(resolve, ms);
} else {
reject(console.log("Shop is closed"));
}
});
}
async function kitchen() {
try {
await time(2000);
console.log(`${stocks.Fruits[0]} was selected`);
await time(0);
console.log("production has started");
await time(2000);
console.log("fruit has been chopped");
await time(1000);
console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`);
await time(1000);
console.log("start the machine");
await time(2000);
console.log(`ice cream placed on ${stocks.holder[1]}`);
await time(3000);
console.log(`${stocks.toppings[0]} as toppings`);
await time(2000);
console.log("Serve Ice Cream");
} catch (error) {
console.log("customer left");
} finally {
console.log("Day ended, shop closed");
}
}
kitchen();

متى تستخدم Callbacks ومتى تستخدم Promises أو Async/Await؟
| الأسلوب | أفضل استخدام | الميزة الأساسية | التحدي |
|---|---|---|---|
Callbacks |
المهام البسيطة جدًا | مباشر وسريع | يصعب تنظيمه عند التوسع |
Promises |
تسلسل العمليات غير المتزامنة | تنظيم أفضل ومعالجة أوضح للأخطاء | قد يطول التسلسل مع كثرة .then() |
Async/Await |
المشاريع الحديثة والكود المقروء | وضوح كبير وبنية قريبة من الكود المتزامن | يتطلب فهمًا جيدًا لـPromises |
أفضل ممارسات عند التعامل مع البرمجة غير المتزامنة
- ابدأ بفهم
Promisesأولًا قبل الاعتماد الكامل علىAsync/Await. - استخدم
try/catchدائمًا عند وجود احتمال لفشل العملية. - لا تفرط في تداخل
callbacksإذا كان بالإمكان استخدام حل أوضح. - حافظ على فصل منطق البيانات عن منطق العرض لتسهيل الصيانة.
- استخدم أسماء دوال ومتغيرات معبرة مثل
orderوproductionوtime.
الخلاصة التقنية
فهم البرمجة غير المتزامنة في JavaScript ليس مجرد موضوع نظري، بل هو مهارة أساسية لأي مطور يعمل على تطبيقات الويب الحديثة. تبدأ الرحلة غالبًا مع Callbacks، ثم تتطور إلى Promises لحل مشكلة التداخل، وأخيرًا تصل إلى Async/Await الذي يوفر أسلوبًا أكثر نظافة ووضوحًا. من الناحية العملية، يظل Async/Await هو الخيار الأفضل في أغلب المشاريع الحديثة، لكن إتقانه الحقيقي يتطلب فهمًا عميقًا لكيفية عمل Promise نفسها.