قد يكون العمل مع الدوال غير المتزامنة ممتعاً ومثيراً للاهتمام، إذ يمكن لرحلة البرمجة غير المتزامنة أن تستمر خارج نطاق الدوال قيد التنفيذ، مما يضمن بنية برمجية غير معطِّلة للإدخال والإخراج. وتنتشر في واجهات البرمجة القائمة على الأحداث، وتُستخدم في أجزاء كثيرة من الويب، كعمليات CRUD من جهة المتصفح أو استدعاء خدمة خارجية من جهة الخادم. غير أن التعامل مع عدد متزايد من الدوال غير المتزامنة في البرنامج قد يصبح مرهقاً إن لم يُدَر جيداً.
فالوعود (Promises)، كما نعلم، يمكنها حل النتيجة أو رفضها وتمرير الخطأ إن التقطته دالة catch. وكل ذلك يضيف نطاقات إضافية إلى الدالة المُعدّة أصلاً للحصول على النتيجة المطلوبة، وقد ينشئ كل منها دالة استدعاء جديدة بحسب سير عمل البرنامج، فقد يتجاهل السكربت خطأً معيناً أو يكتفي بتسجيله في وحدة السجلات.
في هذا المقال، سنستعرض أربع طرق مختلفة للتعامل مع الوعود، تحقق جميعها الغرض نفسه لكنها تختلف من حيث قابلية الصيانة و الوضوح و سهولة الإعداد، لنختم بالنهج الأسهل والأكثر كفاءة وفعالية.
استخدمتها جميعاً شخصياً في وقت ما، لكنني بدأت أتجه إلى النهج المتقدم والأسهل صيانة لأجعل حياتي أيسر أثناء البرمجة وأتفرغ للتفكير النقدي في الحل الشامل وسير العمل وغير ذلك..
ستعمل جميع الأمثلة باستخدام Node.js، ولعرض النتائج سنستخدم وحدة Express.js لإظهارها كاستدعاء لخدمة ويب قد يكون جزءاً من سيناريو واقعي.
المثال التالي للتوثيق والمقارنة فقط، فانتقل مباشرة إلى نهاية المقال إن لم ترغب في الخوض في هذه الأساسيات.
#1 دوال الاستدعاء - Callback (ES5)
يستخدم المثال «
https://jsonplaceholder.typicode.com» مزوّداً للخدمة لتوليد كائن JSON صالح للعرض التوضيحي. ولنفترض أننا نريد إعادة قائمة المعرّفات فقط، فتُمرَّر مصفوفةً إلى استجابة HTTP. وكما ترى، تقع «ids» في نطاق جديد تماماً، مما يجعل ما يُعرَّف فيه أقل ملاءمة إن أردنا البناء على المعرّفات لاحقاً.
#2 Promise.all (ES6)
يستخدم Promise.all كائن الوعد العام، وتقبل الدالة «all» مصفوفة من الوعود؛ تخيّلها مجموعة من عمليات قاعدة البيانات (إنشاء وتحديث) تعمل في آن واحد لكنها تُحل في أوقات مختلفة. وتُطلق هذه الدالة كائن وعد آخر متى حُلّت أو رُفضت جميع الوعود الممرّرة في المصفوفة. وهذا مفيد بالتأكيد وأفضل من الطريقة السابقة (دوال الاستدعاء)، لكنه لا يزال ينشئ نطاقاً جديداً ويعزل النتائج عن الدالة الأم. وسيقدّم المثال التالي حلاً جديداً يحافظ على النطاق الجاري ويختصر السكربت المكتوب.
#3 Async/Await (ES7)
هذا جزء من مواصفات JavaScript الأحدث والثورية، إذ يستخدم الوعود لإعادة النتائج ويجعل كتابة JavaScript تبدو كالبرمجة المتزامنة. وقد يكون هذا النهج مربكاً قليلاً لمستخدمي «الوعود» لأنه يتكوّن من خطوتين: الأولى استدعاء دالة تُعيد كائن «promise»، والثانية تغليف الدالة التي تستدعي الوعد بدالة «async» تُعيد كائن «AsyncFunction». ويستخدم هذا النهج الكلمتين المحجوزتين «async» و«await» كما في المثال التالي:
تغليف النطاق بدالة async أمر لا بد منه، وكان ذلك الجزء الغائب والمربك بالنسبة لي لفترة حين ظهر «async/await» لأول مرة. فـ Await هي حيث يحدث السحر: نتخلص من دالة الاستدعاء المزعجة بالكامل ونُسند الاستجابة، عند حلّها، إلى «const response = ..» المعرّفة. وقد تم جلب كائن «users» بصيغة JSON واستخراج معرّفاته في نطاق واحد بطريقة نظيفة وسهلة مقارنة بالأمثلة الفوضوية السابقة. وهذا ما كنا نسعى إليه منذ البداية، لكن لنلقِ نظرة على النهج التالي.
#4 الدوال المولّدة - Generator Functions (ES6)
المولّدات دوال يمكنها تنفيذ جزء من الدالة ثم الخروج منها والعودة إليها لاحقاً. وحين تُقرن بالوعود، تصبح أداة قوية للغاية يمكن الاعتماد عليها في العمل مع الوعود.
هناك وحدة npm تبسّط العمل مع الدوال المولّدة اسمها «co». وأرى شخصياً أن «co» أداة قوية جداً لبرمجة عالية الإنتاجية والكفاءة وللواجهات غير المتزامنة. ويقدّم المثال التالي دليلاً سريعاً لاستخدام هذه الوحدة لاستخراج كائن المستخدمين بصيغة JSON مجدداً.
هناك طرق مختلفة يمكن للمولّدات بها استخراج الاستجابة، لكن للإيجاز سنعرض طريقة واحدة ونستكشف كيف تعمل.
الكلمة المحجوزة «yield» واضحة بذاتها، وهي جزء من تنفيذ المولّدات. أنيق ونظيف؟ نعم. فلماذا لا نلقي نظرة سريعة على طريقة عمله بإنشاء نسختنا الخاصة من «co» ونرى مدى جودتها؟
المولّدات من الداخل
لننشئ دالتنا المولّدة function* بأنفسنا بدلاً من الاعتماد على وحدة جاهزة.
تُخزَّن الدالة المولّدة الممرّرة وسيطاً بوصفها نسخة من «IteratorFunction» لاستدعائها لاحقاً، وستكون المكرِّر العام الذي يُخرج النتائج من الدالة المولّدة. وبعد إعداد المكرِّر، علينا الوصول إلى الجزء الذي يخزّن «الوعد» من كائن «Iteration»، ثم حلّه للتكرار على محتوى الوعد المحلول مرة أخرى للحصول على النتيجة النهائية.
وإن نظّفنا هذه الفوضى وأعدنا هيكلة دالتنا، فقد تبدو كالتالي:
لنفترض أننا مرّرنا دالة fetch المحبوبة بوصفها دالة مولّدة «(function* () { fetch(‘…’) })». ولأن الوسيط دالة، فلن يُعيد تنفيذها القيمة فوراً، بل يُعيد كائن مكرِّر يُستخدم للتكرار على القيم. وفي كل مرة يُستدعى فيها كائن المكرِّر، يُخزَّن وعد في الخاصية «value»، ويجب حل هذا الوعد للانتقال إلى التكرار التالي. وحين تنتهي الدالة المكرِّرة، تتغيّر قيمة العلامة «done» إلى «true».
الخلاصة
في النهاية، يعود القرار لمطوّر الويب في اختيار ما يراه الأنسب والأنفع للخوارزمية التي يكتبها. وأرى شخصياً أن المولّدات قوية جداً، وهي الأساس الذي بُني عليه تنفيذ async/await. فحين أُطلقت المولّدات ميزةً جديدة في ES6، كانت كمن يحزم ملابسه الشتوية في بداية الصيف؛ أُهملت وتراجعت في القائمة لصالح التركيز على مواصفات مفيدة أخرى.
وأنا متأكد من وجود مجالات كثيرة تكون فيها الدوال المولّدة بالغة الفائدة، كتحسين الوعود ودوال الاستدعاء. فإن كنت تعرف بعضها، شاركنا إياها في تعليق.
وإليك