قبل دراسة محتوى هذه المقالة، يجب علينا أولاً أن نفهم مفهوم غير المتزامن. أول شيء يجب التأكيد عليه هو أن هناك فرقًا أساسيًا بين غير المتزامن والمتوازي .
يشير التوازي عمومًا إلى الحوسبة المتوازية، مما يعني أنه يتم تنفيذ تعليمات متعددة في نفس الوقت، وقد يتم تنفيذ هذه التعليمات على مراكز متعددة لنفس CPU
، أو على CPU
متعددة، أو على مضيفين فعليين متعددين أو حتى شبكات متعددة.
تشير المزامنة عمومًا إلى تنفيذ المهام بترتيب محدد مسبقًا، فقط عند اكتمال المهمة السابقة، سيتم تنفيذ المهمة التالية.
غير متزامن ، يتوافق مع المزامنة، ويعني أن CPU
تضع المهمة الحالية جانبًا مؤقتًا، وتعالج المهمة التالية أولاً، ثم تعود إلى المهمة السابقة لمواصلة التنفيذ بعد تلقي إشعار رد الاتصال بالمهمة السابقة الموضوع الثاني المشاركة .
ربما يكون من البديهي شرح التوازي والتزامن وعدم التزامن في شكل صور، افترض أن هناك مهمتين A وB تحتاجان إلى المعالجة، ستعتمد طرق المعالجة المتوازية والمتزامنة وغير المتزامنة طرق التنفيذ كما هو موضح في الشكل الشكل التالي:
توفر لنا JavaScript
العديد من الوظائف غير المتزامنة، وتسمح لنا هذه الوظائف بتنفيذ المهام غير المتزامنة بسهولة، أي أننا نبدأ في تنفيذ مهمة (وظيفة) الآن، ولكن سيتم إكمال المهمة لاحقًا، وفي الوقت المحدد لإكمالها. ليس متأكدا.
على سبيل المثال، تعتبر الدالة setTimeout
دالة غير متزامنة نموذجية للغاية، بالإضافة إلى ذلك، فإن fs.readFile
و fs.writeFile
هما أيضًا دالتان غير متزامنتين.
يمكننا تحديد حالة مهمة غير متزامنة بأنفسنا، مثل تخصيص وظيفة نسخ الملف copyFile(from,to)
:
const fs = require('fs')function CopyFile(from, to) { fs.readFile(from, (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } fs.writeFile(to, data, (err) => { إذا (يخطئ) { console.log(err.message) يعود } console.log('انتهى النسخ') }) })}
تقوم الدالة copyFile
أولاً بقراءة بيانات الملف من المعلمة from
، ثم تكتب البيانات إلى الملف الذي تشير إليه المعلمة to
.
يمكننا استدعاء copyFile
مثل هذا:
CopyFile('./from.txt','./to.txt')// انسخ الملف
إذا كان هناك رمز آخر بعد copyFile(...)
في هذا الوقت، فلن يقوم البرنامج بذلك انتظر ينتهي تنفيذ copyFile
، لكنه يتم تنفيذه مباشرة للأسفل. لا يهتم البرنامج عندما تنتهي مهمة نسخ الملف.
CopyFile('./from.txt','./to.txt')// لن ينتظر الكود التالي انتهاء تنفيذ الكود أعلاه...
في هذه المرحلة، يبدو كل شيء طبيعيًا، ولكن إذا ماذا يحدث إذا قمت بالوصول مباشرة إلى محتويات الملف ./to.txt
بعد وظيفة copyFile(...)
؟
لن يقرأ هذا المحتوى المنسوخ، تمامًا مثل هذا:
CopyFile('./from.txt','./to.txt')fs.readFile('./to.txt',(err,data)= >{ ...})
إذا لم يتم إنشاء الملف ./to.txt
قبل تنفيذ البرنامج، فسوف تحصل على الخطأ التالي:
PS E:CodeNodedemos 3-callback> عقدة .index.js انتهى انتهت النسخة PS E:CodeNodedemos 3-callback> العقدة .index.js خطأ: ENOENT: لا يوجد مثل هذا الملف أو الدليل، افتح 'E:CodeNodedemos 3-callbackto.txt' انتهى النسخ
حتى في حالة وجود ./to.txt
، لا يمكن قراءة المحتوى المنسوخ.
سبب هذه الظاهرة هو: يتم تنفيذ copyFile(...)
بشكل غير متزامن بعد أن ينفذ البرنامج وظيفة copyFile(...)
، فإنه لا ينتظر اكتمال النسخة، بل ينفذها مباشرة إلى الأسفل، مما يتسبب في الملف. لتظهر خطأ ./to.txt
غير موجود، أو أن محتوى الملف فارغ (إذا تم إنشاء الملف مسبقًا).
لا يمكن تحديد وقت انتهاء التنفيذ المحدد للوظيفة غير المتزامنة لوظيفة رد الاتصال. على سبيل المثال، من المرجح أن يعتمد وقت انتهاء تنفيذ وظيفة readFile(from,to)
على حجم الملف from
.
لذا، السؤال هو كيف يمكننا تحديد موقع نهاية تنفيذ copyFile
بدقة وقراءة محتويات to
؟
يتطلب ذلك استخدام وظيفة رد الاتصال، ويمكننا تعديل وظيفة copyFile
على النحو التالي:
function CopyFile(from, to, callback) { fs.readFile(from, (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } fs.writeFile(to, data, (err) => { إذا (يخطئ) { console.log(err.message) يعود } console.log('انتهى النسخ') رد الاتصال () // يتم استدعاء وظيفة رد الاتصال عند اكتمال عملية النسخ}) })}
بهذه الطريقة، إذا أردنا إجراء بعض العمليات مباشرة بعد اكتمال نسخة الملف، فيمكننا كتابة هذه العمليات في وظيفة رد الاتصال:
function CopyFile(from, to, callback) { fs.readFile(from, (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } fs.writeFile(to, data, (err) => { إذا (يخطئ) { console.log(err.message) يعود } console.log('انتهى النسخ') رد الاتصال () // يتم استدعاء وظيفة رد الاتصال عند اكتمال عملية النسخ}) })}copyFile('./from.txt', './to.txt', الوظيفة () { // تمرير وظيفة رد الاتصال، وقراءة محتويات ملف "to.txt" وإخراج fs.readFile('./to.txt', (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } console.log(data.toString()) })})
إذا قمت بإعداد ملف ./from.txt
، فيمكن تشغيل التعليمات البرمجية أعلاه مباشرة:
PS E:CodeNodedemos 3-callback> عقدة .index.js انتهت النسخة انضم إلى مجتمع "Xianzong" وازرع الخلود معي. عنوان المجتمع: http://t.csdn.cn/EKf1h
تسمى طريقة البرمجة هذه بأسلوب برمجة غير متزامن "قائم على رد الاتصال" ويجب أن توفر الوظائف المنفذة بشكل غير متزامن معلمات رد الاتصال تستخدم للاتصال بعد انتهاء المهمة.
هذا النمط شائع في برمجة JavaScript
، على سبيل المثال، وظائف قراءة الملفات fs.readFile
و fs.writeFile
كلها وظائف غير متزامنة.
يمكن لوظيفة رد الاتصال التعامل بدقة مع الأمور اللاحقة بعد اكتمال العمل غير المتزامن. إذا كنا بحاجة إلى تنفيذ عمليات متعددة غير متزامنة بالتسلسل، فسنحتاج إلى تداخل وظيفة رد الاتصال.
سيناريو الحالة:
تنفيذ التعليمات البرمجية لقراءة الملف A والملف B بالتسلسل:
fs.readFile('./A.txt', (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } console.log("قراءة الملف أ:" + data.toString()) fs.readFile('./B.txt', (err, data) => { إذا (يخطئ) { console.log(err.message) يعود } console.log("قراءة الملف ب:" + data.toString()) })})
تأثير التنفيذ:
PS E:CodeNodedemos 3-callback> عقدة .index.js قراءة الملف A: Immortal Sect جيد للغاية، لكنه يفتقد شخصًا ما. قراءة الملف B: إذا كنت تريد الانضمام إلى Immortal Sect، فيجب أن يكون لديك الرابط http://t.csdn.cn/H1faI،
يمكنك القراءة بعد A، تتم قراءة الملف B على الفور.
ماذا لو أردنا مواصلة قراءة الملف C بعد الملف B؟ يتطلب هذا الاستمرار في تداخل عمليات رد الاتصال:
fs.readFile('./A.txt', (err, data) => {// رد الاتصال الأول if (err) { console.log(err.message) يعود } console.log("قراءة الملف أ:" + data.toString()) fs.readFile('./B.txt', (err, data) => {// رد الاتصال الثاني إذا (يخطئ) { console.log(err.message) يعود } console.log("قراءة الملف ب:" + data.toString()) fs.readFile('./C.txt',(err,data)=>{// رد الاتصال الثالث... }) })})
بمعنى آخر، إذا أردنا تنفيذ عمليات متعددة غير متزامنة بالتسلسل، فإننا نحتاج إلى مستويات متعددة من عمليات الاسترجاعات المتداخلة، ويكون هذا فعالًا عندما يكون عدد المستويات صغيرًا، ولكن عندما يكون هناك عدد كبير جدًا من مرات التداخل، فستحدث بعض المشكلات يحدث.
اصطلاحات رد الاتصال
في الواقع، نمط وظائف رد الاتصال في fs.readFile
ليس استثناءً، ولكنه تقليد شائع في JavaScript
. سنقوم بتخصيص عدد كبير من وظائف رد الاتصال في المستقبل، وعلينا الالتزام بهذه الاتفاقية وتكوين عادات ترميز جيدة.
الاصطلاح هو:
callback
محجوزة للخطأ. بمجرد حدوث خطأ، سيتم استدعاء callback(err)
.callback(null, result1, result2,...)
.بناءً على الاتفاقية المذكورة أعلاه، فإن وظيفة رد الاتصال لها وظيفتان: معالجة الأخطاء واستقبال النتائج، على سبيل المثال، تتبع وظيفة رد الاتصال fs.readFile('...',(err,data)=>{})
هذه الاتفاقية.
إذا لم نتعمق أكثر، فيبدو أن المعالجة غير المتزامنة للطرق بناءً على عمليات الاسترجاعات هي طريقة مثالية جدًا للتعامل معها. المشكلة هي أنه إذا كان لدينا سلوك غير متزامن تلو الآخر، فإن الكود سيبدو كما يلي:
fs.readFile('./a.txt',(err,data)=>{ إذا (خطأ){ console.log(err.message) يعود } // قراءة عملية النتيجة fs.readFile('./b.txt',(err,data)=>{ إذا (خطأ){ console.log(err.message) يعود } // قراءة عملية النتيجة fs.readFile('./c.txt',(err,data)=>{ إذا (خطأ){ console.log(err.message) يعود } // قراءة عملية النتيجة fs.readFile('./d.txt',(err,data)=>{ إذا (خطأ){ console.log(err.message) يعود } ... }) }) })})
محتوى تنفيذ الكود أعلاه هو:
مع زيادة عدد الاستدعاءات، يصبح مستوى تداخل التعليمات البرمجية أعمق فأعمق، بما في ذلك المزيد والمزيد من العبارات الشرطية، مما يؤدي إلى إرباك التعليمات البرمجية التي يتم وضع مسافة بادئة لها باستمرار إلى اليمين، مما يجعل من الصعب قراءتها و يحافظ على.
نحن نسمي ظاهرة النمو المستمر إلى اليمين (المسافة البادئة إلى اليمين) " رد الاتصال بالجحيم " أو " هرم الهلاك "!
fs.readFile('a.txt',(err,data)=>{ fs.readFile('b.txt',(err,data)=>{ fs.readFile('c.txt',(err,data)=>{ fs.readFile('d.txt',(err,data)=>{ fs.readFile('e.txt',(err,data)=>{ fs.readFile('f.txt',(err,data)=>{ fs.readFile('g.txt',(err,data)=>{ fs.readFile('h.txt',(err,data)=>{ ... /* بوابة الجحيم ===> */ }) }) }) }) }) }) })})
على الرغم من أن الكود أعلاه يبدو عاديًا تمامًا، إلا أنه مجرد موقف مثالي على سبيل المثال، عادةً ما يكون هناك عدد كبير من العبارات الشرطية وعمليات معالجة البيانات والرموز الأخرى في منطق الأعمال، مما يعطل النظام الجميل الحالي ويجعل. تغيير التعليمات البرمجية يصعب الحفاظ عليه.
ولحسن الحظ، توفر لنا JavaScript
حلولاً متعددة، ويعتبر Promise
هو الحل الأفضل.