لتوضيح استخدام الاسترجاعات والوعود والمفاهيم المجردة الأخرى ، سنستخدم بعض طرق المتصفح: على وجه التحديد ، تحميل البرامج النصية وأداء التلاعب بالمستندات البسيطة.
إذا لم تكن على دراية بهذه الطرق ، وكان استخدامها في الأمثلة مربكًا ، فقد ترغب في قراءة بعض الفصول من الجزء التالي من الدورة التعليمية.
على الرغم من أننا سنحاول توضيح الأمور على أي حال. لن يكون هناك أي شيء معقد بالفعل في المتصفح.
يتم توفير العديد من الوظائف من خلال بيئات استضافة JavaScript التي تسمح لك بجدولة الاحداث الغير متزامنة. بعبارة أخرى ، الإجراءات التي نبدأها الآن ، لكنها تنتهي لاحقًا.
علي سبيل المثال, داله واحده مثل دالة ال setTimeout
.
هناك أمثلة أخرى على أرض الواقع من الإجراءات غير المتزامنة ، مثل تحميل البرامج النصية والوحدات (سنغطيها في الفصول اللاحقة).
ألق نظرة علي الدالة loadScript(src)
, والتي تقوم بتحميل برنامج نصي عند أعطاءها مصدر البرنامج src
:
function loadScript(src) {
// أنشئ العنصر <script> و قم بأضافته الي الصفحة
// هذا الذي يفعله البرنامج النصي عند أعطاءه ال src لكي يبدأ تحميل ومن ثم يشغل البرنامج عند الاكتمال
let script = 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'); // البرنامج النصي يمتلك "function newFunction() {…}"
newFunction(); // لا يوجد مثل هذه الوظيفة!
بطبيعة الحال ، ربما لم يكن لدى المتصفح وقت لتحميل البرنامج النصي. اعتبارا من الآن، الدالة loadScript
لا توفر الوظيفة طريقة لتتبع إكمال التحميل. يتم تحميل البرنامج النصي وتشغيله في النهاية ، هذا كل ما في الأمر. ولكن نود أن نعرف متى يحدث ذلك ، لاستخدام وظائف ومتغيرات جديدة من هذا البرنامج النصي.
دعنا نضيف وظيفة callback
كوسيطة ثانية إلىloadScript
التي يجب تنفيذها عند تحميل البرنامج النصي:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
الآن إذا أردنا استدعاء وظائف جديدة من البرنامج النصي ، يجب أن نكتب ذلك في رد الاتصال:
loadScript('/my/script.js', function() {
// يعمل دالة ال callback بعد تحميل البرنامج النصي
newFunction(); // حتى الآن يعمل
...
});
هذه هي الفكرة: الوسيطة الثانية هي وظيفة (عادة ما تكون مجهولة المصدر) يتم تشغيلها عند اكتمال الإجراء.
إليك مثال قابل للتشغيل مع نص حقيقي:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // دالة معلن عنها في البرنامج النصي المحمل
});
وهذا ما يسمى نمط “قائم على الاسترجاع” للبرمجة غير المتزامنة. يجب أن توفر الدالة التي تفعل شيئًا بشكل غير متزامن وسيطة callback
حيث نضع الوظيفة قيد التشغيل بعد اكتمالها.
هنا فعلنا ذلك loadScript
, ولكن بالطبع هذا نهج عام.
Callback في callback
كيف يمكننا تحميل نصين على التوالي: الأول والثاني بعده؟
الحل الطبيعي أن نضع أستدعاء ال loadScript
الثاني داخل دالة الcallback, مثل هذا:
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
بعد اكتمال “loadScript” الخارجي ، يبدأ الاستدعاء الداخلي.
ماذا لو أردنا اكثر من برنامج نصي آخر …؟
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...استمر بعد تحميل جميع البرامج النصية
});
});
});
لذا ، فإن كل عمل جديد يكون داخل دالة ال callback. هذا جيد لبعض الإجراءات ، ولكن ليس جيدًا للعديد ، لذلك سنرى متغيرات أخرى قريبًا.
معالجة الأخطاء
في الأمثلة المذكورة أعلاه لم نعتبر الأخطاء. ماذا لو فشل تحميل البرنامج النصي؟ يجب أن يكون دالة callback الخاص بنا قادرًا على الرد على ذلك.
فيما يلي نسخة محسنة من loadScript
الذي يتتبع أخطاء التحميل:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
أنها تستدعي callback(null, script)
للتحميل الناجح وتستدعي callback(error)
لغير ذلك.
الاستخدام:
loadScript('/my/script.js', function(error, script) {
if (error) {
// معالجة الخطأ هنا
} else {
// البرنامج النصي تم تحميله بنجاح
}
});
مره أخري, الوصفة التي أستخدامناها لل loadScript
هي في الواقع شائعة جداً. تُسمي النمط “error-first callback”.
الاتفاقية هي:
- الوسيطة الأولى لـ
callback
محجوزة لخطأ إذا حدث. ثم يتم أستدعاء الcallback(err)
. - الوسيطة الثانية (وواحدة اخري أذا احتاجنا) تكون للنتيجة الناجحة. ثم يتم أستدعاء ال
callback(null, result1, result2…)
.
لذلك يتم استخدام وظيفة callback
الفردية للإبلاغ عن الأخطاء وإرجاع النتائج.
هرم الموت
من النظرة الأولى ، إنها طريقة قابلة للتطبيق للتشفير غير المتزامن. وهو كذلك بالفعل. بالنسبة لأستدعاء واحد أو ربما أستدعائين متداخليين ، تبدو جيدة.
ولكن بالنسبة إلى العديد من الإجراءات غير المتزامنة التي تتبع واحدًا تلو الآخر ، سيكون لدينا كود مثل هذا:
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});
}
});
}
});
في الكود الذي بالأعلي:
- نحن نحمل ال
1.js
, أذا لم يكن هناك خطأ. - نحن نحمل ال
2.js
, أذا لم يكن هناك خطأ. - نحن نحمل ال
3.js
, أذا لم يكن هناك خطأ – أفعل بعض الشئ غيره(*)
.
عندما تصبح الأستدعاءات أكثر تداخلًا ، تصبح الشفرة أعمق وتزداد صعوبة إدارتها ، خاصة إذا كان لدينا رمز حقيقي بدلاً من ...
قد يتضمن المزيد من الحلقات ، والعبارات الشرطية وما إلى ذلك.
وهذا ما يُطلق عليه أحيانًا “أسترجاع الجحيم” أو “هرم الموت”.
ينمو “هرم” الأستدعاءات المتداخلة إلى اليمين مع كل إجراء غير متزامن. سرعان ما يخرج عن السيطرة.
لذا فإن طريقة كتابة الكود هذه ليست جيدة جدًا.
يمكننا محاولة التخفيف من المشكلة عن طريق جعل كل عمل وظيفة قائمة بذاتها ، مثل هذا:
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...يتم تنفيذ الاكواد هنا بعد تحميل كل الملفات (*)
}
}
نرى؟ إنه يفعل نفس الشيء ، ولا يوجد تداخل عميق الآن لأننا جعلنا كل إجراء وظيفة منفصلة من المستوى الأعلى.
إنه يعمل ، لكن الرمز يبدو وكأنه جدول بيانات ممزق. من الصعب قراءتها ، وربما لاحظت أن المرء يحتاج إلى القفز بين القطع أثناء قراءتها. هذا غير مريح ، خاصة إذا لم يكن القارئ على دراية بالكود ولا يعرف أين يقفز.
أيضاً, الدوال التي تم تسميتها step*
كلها تستخدم علي حدي, يتم إنشاؤها فقط لتجنب “هرم الموت”. لا أحد سيعيد استخدامها خارج سلسلة العمل. لذلك هناك القليل من فوضى مساحة الاسم هنا.
نود الحصول على شيء أفضل.
لحسن الحظ ، هناك طرق أخرى لتجنب مثل هذه الأهرامات. أحد أفضل الطرق هو استخدام “promises” الموضحة في الفصل التالي.