async/await مع forEach في JavaScript: دليل عملي للتعامل مع الحلقات غير المتزامنة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

لماذا لا يستجيب async/await داخل forEach؟ تحليل المشكلة من الجذور

\n\n

هل سبق لك أن كتبت كوداً مثل هذا، على أمل أن يتم تنفيذ العمليات غير المتزامنة بالترتيب؟

\n\n
const 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 تستمر وكأن شيئاً لم يحدث، مثل عداء لا يهتم بمن خلفه.

\n\n

في هذا المقال، لن نعرض فقط كيفية حل المشكلة. سنشرح لماذا تحدث، وسنقدم لك ثلاثة حلول عملية مع أمثلة. ستتعرف متى تستخدم Promise.all ومتى تستخدم for...of التقليدي. هيا بنا!

\n\n

تشريح المشكلة: forEach والوعود (Promises)

\n\n

دعنا نلقي نظرة تحت الغطاء. طريقة forEach في JavaScript تعمل هكذا:

\n\n
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 تتجاهله. لا تنتظر، لا تجمع النتائج - فقط تُشغّل وتنسى.

\n\n

عندما تكتب async (user) => { ... }، فإن الدالة تُرجع Promise. لكن forEach لا تعرف ماذا تفعل به. النتيجة أن جميع الطلبات تُنفذ بالتوازي (إذا كانت fetch)، لكنك لا تستطيع انتظار انتهائها. الكود بعد forEach يُنفذ فوراً.

\n\n

مثال واقعي: كيف يبدو الأمر عملياً

\n\n

تخيل أنك تعالج مصفوفة من أرقام المنتجات وتحتاج للحصول على سعر كل منتج من قاعدة البيانات:

\n\n
const 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 بشكل طبيعي لأنك تتحكم في تدفق التنفيذ داخل الكتلة.

\n\n
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

العيوب: بطيء - كل طلب ينتظر انتهاء الطلب السابق.

المدونات

توصيات الكتب