لماذا لا يستجيب async/await داخل forEach؟ تحليل المشكلة من الجذور
\n\nهل سبق لك أن كتبت كوداً مثل هذا، على أمل أن يتم تنفيذ العمليات غير المتزامنة بالترتيب؟
\n\nconst users = ['ahmed', 'sara', 'khaled'];\n\nusers.forEach(async (user) => {\n const data = await fetch(`https://api.example.com/users/${user}`);\n console.log(await data.json());\n});\n\nconsole.log('تم الانتهاء!');\n\nولكنك وجدت أن "تم الانتهاء!" يظهر قبل تحميل البيانات؟ هذا مألوف؟ لست وحدك. هذا أحد أكثر الأسئلة شيوعاً على Stack Overflow بمشاهدات تتجاوز الآلاف. المشكلة أن forEach هي دالة متزامنة. هي فقط تُشغّل الدوال callback ولكنها لا تنتظر انتهاءها. دوالك غير المتزامنة تبدأ، لكن forEach تستمر وكأن شيئاً لم يحدث، مثل عداء لا يهتم بمن خلفه.
في هذا المقال، لن نعرض فقط كيفية حل المشكلة. سنشرح لماذا تحدث، وسنقدم لك ثلاثة حلول عملية مع أمثلة. ستتعرف متى تستخدم Promise.all ومتى تستخدم for...of التقليدي. هيا بنا!
تشريح المشكلة: forEach والوعود (Promises)
\n\nدعنا نلقي نظرة تحت الغطاء. طريقة forEach في JavaScript تعمل هكذا:
Array.prototype.forEach = function(callback) {\n for (let i = 0; i < this.length; i++) {\n callback(this[i], i, this);\n }\n};\n\nأترى؟ هي فقط تستدعي callback لكل عنصر، لكنها لا تُرجع شيئاً. حتى لو أعادت الدالة callback وعداً (Promise)، فإن forEach تتجاهله. لا تنتظر، لا تجمع النتائج - فقط تُشغّل وتنسى.
عندما تكتب async (user) => { ... }، فإن الدالة تُرجع Promise. لكن forEach لا تعرف ماذا تفعل به. النتيجة أن جميع الطلبات تُنفذ بالتوازي (إذا كانت fetch)، لكنك لا تستطيع انتظار انتهائها. الكود بعد forEach يُنفذ فوراً.
مثال واقعي: كيف يبدو الأمر عملياً
\n\nتخيل أنك تعالج مصفوفة من أرقام المنتجات وتحتاج للحصول على سعر كل منتج من قاعدة البيانات:
\n\nconst productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n // محاكاة طلب قاعدة بيانات\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`سعر المنتج ${id}: $${Math.floor(Math.random() * 100)}`);\n }, 1000);\n });\n}\n\nproductIds.forEach(async (id) => {\n const price = await getPrice(id);\n console.log(price);\n});\n\nconsole.log('تم تحميل جميع الأسعار!'); // سيتم التنفيذ فوراً، وليس بعد 3 ثوانٍ\n\nالنتيجة في وحدة التحكم:
\nتم تحميل جميع الأسعار!\nسعر المنتج 101: $42\nسعر المنتج 102: $78\nسعر المنتج 103: $15\n\nأليس هذا ما توقعته؟ الخبر السار: هناك حلول بسيطة وأنيقة.
\n\nالحل الأول: for...of — التنفيذ التسلسلي
\n\nأسهل طريقة لجعل async/await يعمل داخل حلقة هي استخدام for...of. على عكس forEach، فإن for...of تعمل مع async/await بشكل طبيعي لأنك تتحكم في تدفق التنفيذ داخل الكتلة.
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`سعر المنتج ${id}: $${Math.floor(Math.random() * 100)}`);\n }, 1000);\n });\n}\n\nasync function loadAllPrices() {\n for (const id of productIds) {\n const price = await getPrice(id);\n console.log(price);\n }\n console.log('تم تحميل جميع الأسعار!');\n}\n\nloadAllPrices();\n\nالآن ستكون النتيجة:
\nسعر المنتج 101: $42\n(بعد ثانية واحدة)\nسعر المنتج 102: $78\n(بعد ثانية واحدة)\nسعر المنتج 103: $15\n(بعد ثانية واحدة)\nتم تحميل جميع الأسعار!\n\nالمزايا: البساطة، سهولة القراءة، ترتيب يمكن التنبؤ به.
\nالعيوب: بطيء - كل طلب ينتظر انتهاء الطلب السابق.