فهم async و await في JavaScript: شرح عملي للبرمجة غير المتزامنة
عندما يطلب تطبيق JavaScript بيانات من خادم بعيد، لا ينبغي للصفحة أن تتوقف حتى تصل الاستجابة. لهذا تعتمد JavaScript على البرمجة غير المتزامنة، وهي طريقة تسمح بتنفيذ العمل الطويل في الخلفية ثم متابعة البرنامج عند اكتماله. يواجه المبتدئ عادةً مفاهيم مثل Promise و then و catch، ثم يجد async و await أسهل في القراءة لكنه لا يعرف ما يحدث خلفهما. يشرح هذا المقال الفكرة من البداية من خلال مثال لجلب قائمة مستخدمين، ثم يضيف معالجة للأخطاء وإظهار حالة التحميل. الهدف أن تفهم ترتيب التنفيذ، لا أن تحفظ صيغة واحدة وتستخدمها من دون معرفة حدودها.
الفكرة الأساسية
الـ Promise كائن يمثل نتيجة عملية لم تنته بعد. قد تنجح العملية وتعيد قيمة، أو تفشل وتعيد خطأ. عند استخدام await داخل دالة تحمل async، ينتظر التنفيذ نتيجة الوعد من دون أن يحجب بقية الصفحة. هذا الانتظار يخص الدالة الحالية فقط، ولا يحول JavaScript إلى برنامج متوقف بالكامل. لذلك يمكن للمتصفح الاستمرار في استقبال النقرات وتنفيذ مهام أخرى أثناء انتظار الشبكة. أما try و catch فيسمحان بجمع أخطاء الاتصال وأخطاء التحويل إلى JSON في مكان واحد، وهو ما يجعل الكود أوضح.
مثال برمجي عملي
const statusElement = document.querySelector("#status");
const listElement = document.querySelector("#users");
async function loadUsers() {
statusElement.textContent = "جار تحميل البيانات...";
try {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
listElement.innerHTML = "";
users.forEach((user) => {
const item = document.createElement("li");
item.textContent = user.name;
listElement.appendChild(item);
});
statusElement.textContent = "اكتمل التحميل";
} catch (error) {
statusElement.textContent = "تعذر تحميل البيانات";
console.error(error);
}
}
loadUsers();
شرح المثال
يبدأ المثال بتحديد عنصري الحالة والقائمة من الصفحة، ثم يغير رسالة الحالة قبل إرسال الطلب. تستقبل fetch عنوان المورد وتعيد Promise، ولذلك نستخدم await لانتظار الاستجابة. وجود استجابة من الخادم لا يعني أن الطلب نجح؛ فقد يكون الرمز 404 أو 500. لهذا فحصنا response.ok قبل قراءة JSON. بعد نجاح الفحص نقرأ البيانات، نفرغ القائمة، ثم نضيف عنصراً لكل مستخدم. إذا وقع خطأ في الاتصال أو في قراءة البيانات، تنتقل العملية إلى catch وتعرض رسالة مفهومة بدلاً من ترك الصفحة في حالة صامتة.
من المفيد فصل جلب البيانات عن رسمها على الشاشة عندما يكبر المشروع. يمكن إنشاء دالة اسمها renderUsers تستقبل المصفوفة، وتترك loadUsers مسؤولة عن الاتصال فقط. كما ينبغي منع الضغط المتكرر على زر التحميل، أو إلغاء الطلب القديم عندما يبدأ المستخدم بحثاً جديداً. في النماذج التي تعتمد على أكثر من طلب، استخدم Promise.all عندما تكون الطلبات مستقلة، واستخدم التنفيذ المتسلسل عندما يعتمد الطلب الثاني على نتيجة الأول.
طريقة العمل قبل كتابة الكود
قبل فتح محرر النصوص، اكتب النتيجة التي تريد الوصول إليها وحدد المدخلات والمخرجات. يساعد هذا التمرين على كشف الحالات الغامضة مبكراً، مثل قيمة ناقصة أو قائمة فارغة أو طلب لا يصل إلى الخادم. لا تحتاج إلى مخطط كبير للمثال التعليمي، لكنك تحتاج إلى أسماء واضحة وخطوات يمكن اختبارها واحدة بعد أخرى. عندما تتغير الفكرة أثناء الكتابة، عدل التصميم قبل إضافة شروط متفرعة يصعب تتبعها.
قسّم المشكلة إلى أجزاء صغيرة، واجعل كل جزء مسؤولاً عن قرار واحد قدر الإمكان. يمكن أن تكون الأجزاء دوال، أو مكونات، أو طبقات منفصلة بحسب التقنية. لا يعني التقسيم إنشاء ملفات كثيرة، بل يعني أن تعرف أين تبحث عندما يحدث الخطأ. سجّل الافتراضات المهمة بجملة قصيرة، مثل أن القائمة مرتبة أو أن السعر غير سالب، ثم تحقق منها في المكان المناسب بدلاً من الاعتماد على الذاكرة.
اختبار المثال في حالات مختلفة
لا تختبر المسار الطبيعي فقط. جرّب مدخلاً فارغاً، وقيمة أكبر من المتوقع، وعنصراً غير موجود، وطلباً يصل من دون البيانات المطلوبة. الحالات الحدية تكشف غالباً مشكلات الفهارس والأنواع وترتيب التنفيذ. اكتب النتيجة المتوقعة قبل تشغيل الكود، ثم قارنها بالنتيجة الفعلية. إذا كان الاختبار يمر بالصدفة، أضف شرطاً أو اختباراً أوضح، ولا تكتفِ بطباعة قيمة تبدو منطقية.
- تحقق من المدخلات قبل استخدامها في الحساب أو التخزين.
- أعد رسالة مفهومة ورمزاً مناسباً عند وقوع الخطأ.
- اجعل الاختبارات قابلة للإعادة من دون اعتماد على شبكة أو وقت متغير.
- راجع أثر التغيير على الأجزاء التي تستدعي الدالة أو المكون.
ملاحظات تتعلق بجودة الكود
تظهر جودة الحل في التفاصيل الصغيرة: اسم يشرح الغرض، ودالة لا تجمع مهاماً بعيدة، ورسالة خطأ لا تترك القارئ في حيرة. لا تحاول اختصار كل سطر، فالكود المقروء أفضل من تعبير قصير يحتاج إلى شرح طويل. وفي الوقت نفسه، لا تكرر القاعدة نفسها في أماكن كثيرة؛ انقلها إلى موضع واحد عندما يكون ذلك أوضح. راجع الملف بعد أن يعمل، لأن أول نسخة تركز عادةً على الوصول إلى النتيجة أكثر من قابلية الصيانة.
احتفظ بالإعدادات التي تختلف بين جهاز وآخر خارج الكود، ولا تضع كلمات مرور أو مفاتيح خاصة في المستودع. استخدم سجلات مناسبة أثناء التطوير، ثم راجع ما ينبغي حجبه في بيئة التشغيل. إذا تعامل البرنامج مع بيانات المستخدم، فافصل بين ما يحتاجه التطبيق وما يمكن الاحتفاظ به. هذه الممارسات لا تخص لغة واحدة، بل تقلل المشكلات عندما يكبر المشروع أو يعمل عليه أكثر من شخص.
متى تعرف أن الحل يحتاج إلى تطوير؟
يحتاج المثال التعليمي إلى طبقات إضافية عندما يدخل في نظام حقيقي: قاعدة بيانات، مستخدمون متعددون، مراقبة، اختبارات، وصلاحيات. لا تضف هذه الأجزاء قبل معرفة المشكلة التي تحلها، لكن لا تنقل الكود التجريبي إلى الإنتاج كما هو. راقب حجم البيانات، وعدد الطلبات، ومصدر المدخلات، وما إذا كان الفشل يجب أن يعيد العملية أو يوقفها. اكتب قرارك في مستند صغير أو تعليق يشرح السبب، حتى لا يضطر الفريق إلى تخمينه لاحقاً.
إذا وجدت أن الخطأ يتكرر في أكثر من مكان، فابحث عن قاعدة مشتركة. وإذا أصبح التعديل في ملف صغير يؤثر في ملفات كثيرة، فراجع حدود المسؤوليات. لا توجد بنية واحدة صحيحة لكل مشروع، لكن توجد أسئلة تساعدك على اختيار بنية مناسبة: من يملك البيانات؟ من يغيرها؟ ماذا يحدث عند الفشل؟ وكيف يمكن اختبار الجزء من دون تشغيل النظام كله؟
أخطاء ينبغي تجنبها
من أكثر الأخطاء شيوعاً نسيان await قبل response.json، فيصبح المتغير Promise بدلاً من البيانات. يخطئ بعض المطورين أيضاً في وضع try حول جزء صغير لا يشمل fetch، أو في عرض رسالة نجاح قبل التأكد من response.ok. لا تستخدم catch لإخفاء كل شيء؛ سجل الخطأ أثناء التطوير، وقدم للمستخدم رسالة عامة لا تكشف تفاصيل الخادم. وعند إرسال بيانات من نموذج، تحقق منها قبل الطلب ولا تفترض أن الخادم سيقبلها دائماً.
خطوة تالية مناسبة
بعد إتقان المثال، جرّب إضافة زر لإعادة المحاولة، ثم أضف حالة فارغة عندما لا تعيد الخدمة أي مستخدم. يمكن أيضاً استخدام AbortController لإلغاء طلب يستغرق وقتاً طويلاً، أو لحماية البحث الفوري من وصول استجابة قديمة بعد استجابة أحدث. هذه الخطوات تربط المفهوم بمشكلات تظهر فعلاً في التطبيقات، وتساعدك على اختيار الطريقة المناسبة بدلاً من تحويل كل دالة إلى async بلا سبب.
تجربة صغيرة لفهم الترتيب
لفهم السلوك بصورة أوضح، ضع ثلاث رسائل في أماكن مختلفة: قبل استدعاء الدالة، داخلها قبل await، وبعد استدعائها. ستلاحظ أن الرسالة الموجودة بعد await لا تظهر بالضرورة قبل الرسائل التي تأتي من عمليات أخرى. لا يعني ذلك أن JavaScript تتصرف عشوائياً؛ بل إن حل الوعود يضاف إلى دورة التنفيذ عندما تصبح النتيجة جاهزة. جرّب أيضاً تأخير الاستجابة باستخدام setTimeout، ثم غيّر مكان تحديث واجهة المستخدم. هذه التجربة القصيرة أفضل من حفظ قاعدة عامة، لأنها تربط المفهوم بما تراه فعلياً في أدوات المطور. عندما يصبح ترتيب التنفيذ مفهوماً، ستجد أن معالجة التحميل والأخطاء قرار تصميم واضح وليست مجموعة أوامر متفرقة.
الخلاصة
بعد إتقان المثال، جرّب إضافة زر لإعادة المحاولة، ثم أضف حالة فارغة عندما لا تعيد الخدمة أي مستخدم. يمكن أيضاً استخدام AbortController لإلغاء طلب يستغرق وقتاً طويلاً، أو لحماية البحث الفوري من وصول استجابة قديمة بعد استجابة أحدث. هذه الخطوات تربط المفهوم بمشكلات تظهر فعلاً في التطبيقات، وتساعدك على اختيار الطريقة المناسبة بدلاً من تحويل كل دالة إلى async بلا سبب. يوضح هذا الموضوع كيف يتحول مفهوم نظري إلى خطوات يمكن تشغيلها وفحصها.
ابدأ بتطبيق المثال على ملف صغير، ثم غيّر مدخلاً واحداً وراقب النتيجة. بعد ذلك أضف حالة فشل واكتب اختباراً لها، ثم انقل الفكرة إلى مشروعك الحقيقي بحذر. عندما تفهم سبب كل خطوة، ستستطيع تغيير الأدوات أو اللغة من دون فقدان المفهوم. البرمجة تتحسن بالمحاولات القصيرة والمراجعة المستمرة، لا بنسخ كود طويل من دون معرفة ما الذي يحميه أو ما الذي قد يكسره.