نحن نستخدم أساليب المتصفح في الأمثلة هنا
لتوضيح استخدام عمليات الاسترجاعات والوعود والمفاهيم المجردة الأخرى، سنستخدم بعض أساليب المتصفح: على وجه التحديد، تحميل البرامج النصية وإجراء عمليات معالجة بسيطة للمستندات.
إذا لم تكن على دراية بهذه الطرق، وكان استخدامها في الأمثلة مربكًا، فقد ترغب في قراءة بعض الفصول من الجزء التالي من البرنامج التعليمي.
على الرغم من أننا سنحاول توضيح الأمور على أي حال. لن يكون هناك أي شيء معقد حقًا فيما يتعلق بالمتصفح.
يتم توفير العديد من الوظائف بواسطة بيئات JavaScript المضيفة التي تسمح لك بجدولة الإجراءات غير المتزامنة . بمعنى آخر، الإجراءات التي نبدأها الآن، ولكنها تنتهي لاحقًا.
على سبيل المثال، إحدى هذه الوظائف هي وظيفة setTimeout
.
هناك أمثلة أخرى من العالم الحقيقي للإجراءات غير المتزامنة، على سبيل المثال تحميل البرامج النصية والوحدات النمطية (سنغطيها في الفصول اللاحقة).
ألقِ نظرة على الدالة loadScript(src)
، التي تقوم بتحميل البرنامج النصي باستخدام src
المحدد:
وظيفة تحميل سكريبت (سرك) { // ينشئ علامة <script> ويلحقها بالصفحة // يؤدي هذا إلى بدء تحميل البرنامج النصي الذي يحتوي على src المحدد وتشغيله عند اكتماله دع البرنامج النصي = document.createElement('script'); script.src = src; document.head.append(script); }
يقوم بإدراج علامة جديدة تم إنشاؤها ديناميكيًا في المستند <script src="…">
باستخدام src
المحدد. يبدأ المتصفح تلقائيًا في تحميله وتنفيذه عند اكتماله.
يمكننا استخدام هذه الوظيفة مثل هذا:
// قم بتحميل وتنفيذ البرنامج النصي على المسار المحدد LoadScript('/my/script.js');
يتم تنفيذ البرنامج النصي "بشكل غير متزامن"، حيث يبدأ التحميل الآن، ولكنه يعمل لاحقًا، عندما تنتهي الوظيفة بالفعل.
إذا كان هناك أي كود أسفل loadScript(…)
، فلن ينتظر حتى انتهاء تحميل البرنامج النصي.
LoadScript('/my/script.js'); // الكود الموجود أسفل LoadScript // لا ينتظر انتهاء تحميل البرنامج النصي // ...
لنفترض أننا بحاجة إلى استخدام البرنامج النصي الجديد بمجرد تحميله. يعلن عن وظائف جديدة، ونريد تشغيلها.
لكن إذا فعلنا ذلك فورًا بعد استدعاء loadScript(…)
، فلن ينجح ذلك:
LoadScript('/my/script.js'); // يحتوي البرنامج النصي على "وظيفة newFunction() {...}" newFunction(); // لا توجد مثل هذه الوظيفة!
وبطبيعة الحال، ربما لم يكن لدى المتصفح الوقت الكافي لتحميل البرنامج النصي. اعتبارًا من الآن، لا توفر وظيفة loadScript
طريقة لتتبع اكتمال التحميل. يتم تحميل البرنامج النصي وتشغيله في النهاية، هذا كل شيء. لكننا نود أن نعرف متى يحدث ذلك، لاستخدام وظائف ومتغيرات جديدة من هذا البرنامج النصي.
لنقم بإضافة دالة callback
كوسيطة ثانية إلى loadScript
والتي يجب تنفيذها عند تحميل البرنامج النصي:
وظيفة تحميل سكريبت (src، رد الاتصال) { دع البرنامج النصي = document.createElement('script'); script.src = src; script.onload = () => رد الاتصال(script); document.head.append(script); }
تم وصف حدث onload
في المقالة تحميل الموارد: onload وonerror، وهو ينفذ بشكل أساسي وظيفة بعد تحميل البرنامج النصي وتنفيذه.
الآن إذا أردنا استدعاء وظائف جديدة من البرنامج النصي، فيجب علينا كتابة ذلك في رد الاتصال:
LoadScript('/my/script.js', function() { // يتم تشغيل رد الاتصال بعد تحميل البرنامج النصي newFunction(); // حتى الآن يعمل ... });
هذه هي الفكرة: الوسيطة الثانية هي دالة (عادةً ما تكون مجهولة) يتم تشغيلها عند اكتمال الإجراء.
إليك مثال قابل للتشغيل باستخدام برنامج نصي حقيقي:
وظيفة تحميل سكريبت (src، رد الاتصال) { دع البرنامج النصي = document.createElement('script'); script.src = src; script.onload = () => رد الاتصال(script); document.head.append(script); } LoadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { تنبيه ("رائع، تم تحميل البرنامج النصي ${script.src}")؛ يُحذًِر( _ )؛ // _ هي وظيفة معلنة في البرنامج النصي المحمل });
وهذا ما يسمى النمط "القائم على رد الاتصال" للبرمجة غير المتزامنة. يجب أن توفر الوظيفة التي تقوم بشيء ما بشكل غير متزامن وسيطة callback
حيث نقوم بتشغيل الوظيفة بعد اكتمالها.
لقد قمنا بذلك هنا في loadScript
، لكنه بالطبع نهج عام.
كيف يمكننا تحميل نصين بالتتابع: الأول ثم الثاني بعده؟
سيكون الحل الطبيعي هو وضع استدعاء loadScript
الثاني داخل رد الاتصال، مثل هذا:
LoadScript('/my/script.js', function(script) { تنبيه ("رائع، تم تحميل ${script.src}، فلنقم بتحميل واحد آخر")؛ LoadScript('/my/script2.js', function(script) { تنبيه ("رائع، تم تحميل البرنامج النصي الثاني")؛ }); });
بعد اكتمال loadScript
الخارجي، يبدأ رد الاتصال الرد الداخلي.
ماذا لو أردنا نصًا آخر...؟
LoadScript('/my/script.js', function(script) { LoadScript('/my/script2.js', function(script) { LoadScript('/my/script3.js', function(script) { // ...تابع بعد تحميل كافة البرامج النصية }); }); });
لذا، كل إجراء جديد يكون داخل رد اتصال. يعد هذا أمرًا جيدًا بالنسبة لعدد قليل من الإجراءات، ولكنه ليس جيدًا بالنسبة للعديد من الإجراءات، لذلك سنرى متغيرات أخرى قريبًا.
في الأمثلة المذكورة أعلاه لم نأخذ الأخطاء بعين الاعتبار. ماذا لو فشل تحميل البرنامج النصي؟ يجب أن يكون رد الاتصال لدينا قادرًا على الرد على ذلك.
إليك نسخة محسنة من loadScript
تتعقب أخطاء التحميل:
وظيفة تحميل سكريبت (src، رد الاتصال) { دع البرنامج النصي = document.createElement('script'); script.src = src; script.onload = () => رد الاتصال(null, script); script.onerror = () => callback(new Error(`خطأ في تحميل البرنامج النصي لـ ${src}`)); document.head.append(script); }
فإنه يستدعي callback(null, script)
للتحميل الناجح callback(error)
خلاف ذلك.
الاستخدام:
LoadScript('/my/script.js', function(error, script) { إذا (خطأ) { // التعامل مع الخطأ } آخر { // تم تحميل البرنامج النصي بنجاح } });
مرة أخرى، الوصفة التي استخدمناها لـ loadScript
هي في الواقع شائعة جدًا. يطلق عليه نمط "رد الاتصال بالخطأ أولاً".
الاتفاقية هي:
يتم حجز الوسيطة الأولى callback
لخطأ في حالة حدوثه. ثم يتم استدعاء callback(err)
.
الحجة الثانية (والتي تليها إذا لزم الأمر) هي للحصول على نتيجة ناجحة. ثم يتم استدعاء callback(null, result1, result2…)
.
لذلك يتم استخدام وظيفة callback
الفردية للإبلاغ عن الأخطاء وإعادة النتائج.
للوهلة الأولى، يبدو وكأنه نهج قابل للتطبيق للتشفير غير المتزامن. وهو بالفعل كذلك. يبدو الأمر جيدًا لمكالمة واحدة أو ربما مكالمتين متداخلتين.
لكن بالنسبة للإجراءات المتعددة غير المتزامنة التي تتبع واحدة تلو الأخرى، سيكون لدينا رمز مثل هذا:
تحميل سكريبت ('1.js'، وظيفة (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ... تحميل سكريبت ('2.js'، وظيفة (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ... تحميل سكريبت ('3.js'، وظيفة (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ...تابع بعد تحميل كافة البرامج النصية (*) } }); } }); } });
في الكود أعلاه:
نقوم بتحميل 1.js
، ثم إذا لم يكن هناك خطأ...
نقوم بتحميل 2.js
، ثم إذا لم يكن هناك خطأ...
نقوم بتحميل 3.js
، ثم إذا لم يكن هناك خطأ - فافعل شيئًا آخر (*)
.
عندما تصبح الاستدعاءات أكثر تداخلاً، تصبح التعليمات البرمجية أعمق وأكثر صعوبة في إدارتها، خاصة إذا كان لدينا تعليمات برمجية حقيقية بدلاً من ...
والتي قد تتضمن المزيد من الحلقات والعبارات الشرطية وما إلى ذلك.
يُطلق على هذا أحيانًا اسم "جحيم رد الاتصال" أو "هرم الهلاك".
ينمو "هرم" المكالمات المتداخلة إلى اليمين مع كل إجراء غير متزامن. وسرعان ما يخرج الأمر عن نطاق السيطرة.
لذا فإن طريقة الترميز هذه ليست جيدة جدًا.
يمكننا أن نحاول تخفيف المشكلة عن طريق جعل كل إجراء وظيفة مستقلة، مثل هذا:
LoadScript('1.js', step1); وظيفة الخطوة 1 (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ... LoadScript('2.js', step2); } } وظيفة الخطوة 2 (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ... LoadScript('3.js', step3); } } وظيفة الخطوة 3 (خطأ، البرنامج النصي) { إذا (خطأ) { HandleError(error); } آخر { // ...تابع بعد تحميل كافة البرامج النصية (*) } }
يرى؟ إنه يفعل نفس الشيء، ولا يوجد تداخل عميق الآن لأننا جعلنا كل إجراء بمثابة وظيفة منفصلة ذات مستوى أعلى.
إنه يعمل، لكن الكود يبدو وكأنه جدول بيانات ممزق. من الصعب قراءتها، وربما لاحظت أن المرء يحتاج إلى القفز بين القطع أثناء قراءته. وهذا أمر غير مريح، خاصة إذا لم يكن القارئ على دراية بالرمز ولا يعرف أين يجب أن يقفز بعينه.
كما أن الوظائف المسماة step*
كلها ذات استخدام فردي، وقد تم إنشاؤها فقط لتجنب "هرم الهلاك". لن يقوم أحد بإعادة استخدامها خارج سلسلة العمل. لذلك هناك القليل من مساحة الاسم المزدحمة هنا.
نود أن يكون لدينا شيء أفضل.
ولحسن الحظ، هناك طرق أخرى لتجنب مثل هذه الأهرامات. إحدى أفضل الطرق هي استخدام "الوعود"، الموضحة في الفصل التالي.