قبل دراسة محتوى هذه المقالة، يجب علينا أولاً أن نفهم مفهوم غير المتزامن، أول شيء يجب التأكيد عليه هو أن هناك فرقًا أساسيًا بين غير المتزامن والمتوازي .
CPU
، أو على CPU
متعددة، أو على مضيفين فعليين متعددين أو حتى شبكات متعددة.CPU
تضع المهمة الحالية جانبًا مؤقتًا، وتعالج المهمة التالية أولاً، ثم تعود إلى المهمة السابقة لمواصلة التنفيذ بعد تلقي إشعار رد الاتصال بالمهمة السابقة الموضوع الثاني المشاركة .ربما يكون من البديهي شرح التوازي والتزامن وعدم التزامن في شكل صور، افترض أن هناك مهمتين A وB تحتاجان إلى المعالجة، ستعتمد طرق المعالجة المتوازية والمتزامنة وغير المتزامنة طرق التنفيذ كما هو موضح في الشكل الشكل التالي:
توفر لنا JavaScript
العديد من الوظائف غير المتزامنة، وتسمح لنا هذه الوظائف بتنفيذ المهام غير المتزامنة بسهولة، أي أننا نبدأ في تنفيذ مهمة (وظيفة) الآن، ولكن سيتم إكمال المهمة لاحقًا، وفي الوقت المحدد لإكمالها. ليس متأكدا.
على سبيل المثال، تعتبر الدالة setTimeout
دالة غير متزامنة نموذجية للغاية، بالإضافة إلى ذلك، فإن fs.readFile
و fs.writeFile
هما أيضًا دالتان غير متزامنتين.
يمكننا تحديد حالة مهمة غير متزامنة بأنفسنا، مثل تخصيص وظيفة نسخ الملف copyFile(from,to)
:
const fs = require('fs') وظيفة نسخ الملف (من، إلى) { 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: The Immortal Sect جيد للغاية، لكنه يفتقد رجلًا.قراءة الملف B: إذا كنت تريد الانضمام إلى Immortal Sect، فيجب أن يكون لديك روابط.
http://t.csdn.cn/H1faI
من خلال رد الاتصال، يمكنك قراءة الملف B مباشرة بعد قراءة الملف A.
ماذا لو أردنا مواصلة قراءة الملف 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
هو الحل الأفضل.