شرح Async و Await في JavaScript بأسلوب بسيط عبر مثال إعداد البيتزا
ما المقصود بـ Async و Await في JavaScript؟
قد تبدو Async و Await مفاهيم معقدة عند سماعها لأول مرة، لكنها في الواقع من أبسط الأدوات التي تساعد على كتابة كود غير متزامن بطريقة واضحة وسهلة القراءة في JavaScript. الفكرة الأساسية هي تنفيذ مهمة تحتاج إلى وقت، مع الاستمرار في إنجاز مهام أخرى خلال فترة الانتظار بدلاً من تجميد سير البرنامج.
بمعنى آخر، تسمح لك البرمجة غير المتزامنة بأن تطلب نتيجة من عملية تستغرق وقتاً، مثل جلب بيانات من API أو قراءة ملف أو انتظار استجابة من الشبكة، ثم تتابع تنفيذ أجزاء أخرى من البرنامج إلى أن تصبح النتيجة جاهزة.
فهم البرمجة غير المتزامنة من الحياة اليومية
حتى خارج عالم البرمجة، نحن نتعامل مع مهام غير متزامنة بشكل متكرر. هناك أعمال تبدأ الآن، لكن نتيجتها لا تظهر فوراً، وخلال هذا الوقت نستطيع تنفيذ أعمال أخرى.
مثال يومي: طلب الطعام من السيارة
عندما تطلب طعامك من نافذة الطلب، تبدأ العملية أولاً، لكنك لا تبقى متجمداً في مكانك حتى يجهز الطعام. بدلاً من ذلك، تتحرك إلى الأمام في المسار وتنتظر الاستلام عند النافذة التالية. هنا تم بدء المهمة، ثم واصلت تنفيذ خطوة أخرى أثناء انتظار النتيجة.
مثال يومي آخر: تنظيف الأرضية
إذا قمت بمسح أرضية المطبخ، فلن يكون من المنطقي أن تقف بلا حركة حتى تجف. يمكنك خلال ذلك تنظيف غرفة أخرى أو تنفيذ مهمة مختلفة. هذا يشبه تماماً كيف تتعامل JavaScript مع العمليات غير المتزامنة.
شرح Async و Await عبر مثال إعداد البيتزا
لنفترض أنك تريد إعداد بيتزا مجمدة في الفرن. أول خطوة هي تسخين الفرن مسبقاً. بعد تشغيل التسخين، لن تقف مكتوف اليدين حتى يصبح الفرن جاهزاً، بل يمكنك إحضار البيتزا من المجمّد، فتح العلبة، تجهيز الصينية، بل وحتى تناول مشروب أو مشاهدة التلفاز أثناء الانتظار.
هذا هو جوهر async/await: تبدأ مهمة تحتاج وقتاً، ثم تنفذ مهام أخرى، وعندما تصبح النتيجة جاهزة، تتابع الجزء المعتمد عليها.
الكود البرمجي لمحاكاة المثال
// This async function simulates the oven response
const ovenReady = async () => {
return new Promise ( resolve => setTimeout ( () => {
resolve( 'Beep! Oven preheated!' )
}, 3000 ));
}
// Define preheatOven async function
const preheatOven = async () => {
console.log( 'Preheating oven.' );
const response = await ovenReady();
console.log(response);
}
// Define the other functions
const getFrozenPizza = () => console.log( 'Getting pizza.' );
const openFrozenPizza = () => console.log( 'Opening pizza.' );
const getPizzaPan = () => console.log( 'Getting pan.' );
const placeFrozenPizzaOnPan = () => console.log( 'Putting pizza on pan.' );
const grabABeverage = () => console.log( 'Grabbing a beverage.' );
const watchTV = () => console.log( 'Watching television.' );
// Now call the functions
preheatOven();
getFrozenPizza();
openFrozenPizza();
getPizzaPan();
placeFrozenPizzaOnPan();
grabABeverage();
watchTV();
// Output sequence in console:
// Preheating oven.
// Getting pizza.
// Opening pizza.
// Getting pan.
// Putting pizza on pan.
// Grabbing a beverage.
// Watching television.
// Beep! Oven preheated!
كيف يعمل هذا المثال عملياً؟
في هذا المثال، الدالة ovenReady هي دالة async تحاكي انتظار الفرن حتى يسخن. استخدمنا Promise مع setTimeout لتأخير الاستجابة ثلاث ثوانٍ.
أما الدالة preheatOven فهي أيضاً async، وداخلها استخدمنا await لانتظار نتيجة ovenReady. أثناء هذا الانتظار، لا يتوقف البرنامج بالكامل، بل يستمر في تنفيذ الدوال الأخرى مثل:
- getFrozenPizza
- openFrozenPizza
- getPizzaPan
- placeFrozenPizzaOnPan
- grabABeverage
- watchTV
لهذا السبب تظهر رسائل هذه الدوال في console قبل رسالة جهوزية الفرن.
لماذا ظهرت النتائج بهذا الترتيب؟
السبب أن JavaScript لا تنتظر انتهاء الدالة async الخارجية قبل الانتقال إلى التعليمات التالية خارجها. لكنها داخل الدالة نفسها، تتوقف عند await إلى أن تصل النتيجة المطلوبة، ثم تتابع التنفيذ.
أهم قاعدتين لفهم Async/Await في JavaScript
- JavaScript لا توقف البرنامج بالكامل عند استدعاء دالة async مثل preheatOven، لذلك تتابع تنفيذ الأوامر التالية مباشرة.
- الكود داخل الدالة async نفسها سيتوقف عند await إلى أن تكتمل العملية غير المتزامنة وتُرجع النتيجة.
هذه النقطة مهمة جداً، لأنها تفسر لماذا لم يتم تنفيذ console.log(response) إلا بعد انتهاء ovenReady.
متى نستخدم Async و Await فعلياً؟
تُستخدم Async و Await بكثرة في التطبيقات الحقيقية عندما تحتاج إلى التعامل مع عمليات تستغرق وقتاً، مثل:
- جلب البيانات من API.
- إرسال طلبات HTTP.
- قراءة الملفات أو رفعها.
- التعامل مع قواعد البيانات.
- تنفيذ مهام تعتمد على استجابة خارجية.
الميزة الكبرى هنا أن الكود يصبح أكثر وضوحاً مقارنة باستخدام then() المتسلسل، خاصة في المشاريع الكبيرة.
مثال عملي على Async/Await باستخدام Fetch API
إذا لم يكن مثال البيتزا كافياً، فإليك مثالاً واقعياً أكثر يوضح كيفية استخدام Async/Await في جلب البيانات من الإنترنت عبر Fetch API.
const getTheData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw Error();
const data = await response.json();
// do something with this data... save to db, update the DOM, etc.
console.log(data);
console.log('You will see this last.')
} catch (err) {
console.error(err);
}
}
getTheData();
console.log('You will see this first.');
شرح مثال Fetch API خطوة بخطوة
1) تعريف دالة async
أنشأنا دالة باسم getTheData باستخدام async حتى نتمكن من استخدام await داخلها.
2) انتظار استجابة fetch
عند تنفيذ fetch، يرسل المتصفح طلباً إلى الرابط المحدد. استخدمنا await لأن الاستجابة لا تعود فوراً، بل تحتاج وقتاً حتى تصل من الخادم.
3) التحقق من نجاح الاستجابة
استخدمنا الشرط response.ok للتأكد من أن الطلب تم بنجاح. إذا فشل، يتم إطلاق خطأ عبر throw Error().
4) تحويل البيانات إلى JSON
بعد وصول الاستجابة، نحتاج إلى تحويلها إلى صيغة قابلة للاستخدام عبر response.json(). وهذه أيضاً عملية غير متزامنة، لذلك استخدمنا معها await.
5) معالجة الأخطاء عبر try/catch
أحد أفضل أسباب استخدام Async/Await هو سهولة التعامل مع الأخطاء. بنية try/catch تجعل الكود أقرب إلى الأسلوب التقليدي في القراءة وأكثر تنظيماً.
ما الفرق بين async و await؟
| العنصر | الوظيفة |
|---|---|
| async | تُستخدم لتعريف دالة غير متزامنة وتضمن أن الدالة تُرجع Promise. |
| await | تُستخدم داخل دالة async لانتظار نتيجة Promise قبل متابعة التنفيذ. |
بصيغة مختصرة:
- async تعلن أن الدالة تعمل بأسلوب غير متزامن.
- await تجعل الكود ينتظر النتيجة داخل هذه الدالة فقط.
فوائد استخدام Async/Await في JavaScript
- تحسين قابلية قراءة الكود.
- تقليل التعقيد مقارنة بسلاسل Promises.
- تسهيل صيانة المشروع وتطويره.
- تبسيط معالجة الأخطاء باستخدام try/catch.
- جعل تدفق التنفيذ أقرب للفهم البشري والمنطق الطبيعي.
أخطاء شائعة عند استخدام Async/Await
- استخدام await خارج دالة async.
- نسيان معالجة الأخطاء في العمليات الشبكية.
- الاعتقاد بأن await توقف البرنامج بالكامل، بينما هي توقف فقط سياق التنفيذ داخل الدالة الحالية.
- عدم الانتباه إلى أن بعض العمليات يمكن تنفيذها بالتوازي بدلاً من انتظارها بالتسلسل.
متى لا يكون await هو الخيار الأفضل؟
في بعض الحالات، قد لا يكون الانتظار التسلسلي هو الحل الأمثل، خاصة إذا كانت لديك عدة طلبات مستقلة يمكن تنفيذها في الوقت نفسه. عندها قد يكون استخدام Promise.all() أكثر كفاءة من وضع await أمام كل عملية على حدة. لذلك، فهم async/await لا يعني استخدامها في كل موضع، بل اختيارها عندما تكون الأنسب من حيث الوضوح والأداء.
الخلاصة التقنية
Async و Await من أهم ميزات JavaScript الحديثة، لأنهما تمنحانك طريقة نظيفة ومباشرة للتعامل مع العمليات غير المتزامنة دون التضحية بوضوح الكود. مثال البيتزا يوضح الفكرة ببساطة: ابدأ المهمة التي تحتاج وقتاً، ثم استثمر وقت الانتظار في تنفيذ أعمال أخرى. تقنياً، كل مطور JavaScript يحتاج إلى إتقان هذا الأسلوب لأنه أساس التعامل مع Fetch API وواجهات الشبكة والتطبيقات التفاعلية الحديثة.