التحكم في التدفق غير المتزامن
INFO
المادة في هذا المنشور مستوحاة بشكل كبير من كتاب Mixu's Node.js.
في جوهره، تم تصميم جافا سكريبت ليكون غير مُعيق على "الخيط" الرئيسي، وهذا هو المكان الذي يتم فيه عرض العروض. يمكنك تخيل أهمية هذا في المتصفح. عندما يتم حظر الخيط الرئيسي، ينتج عن ذلك "التجميد" سيء السمعة الذي يخشاه المستخدمون النهائيون، ولا يمكن إرسال أي أحداث أخرى مما يؤدي إلى فقدان الحصول على البيانات، على سبيل المثال.
يُنشئ هذا بعض القيود الفريدة التي لا يمكن علاجها إلا بأسلوب وظيفي من البرمجة. هذا هو المكان الذي تأتي فيه المُرتدات إلى الصورة.
ومع ذلك، يمكن أن يصبح التعامل مع المُرتدات أمرًا صعبًا في الإجراءات الأكثر تعقيدًا. غالبًا ما يؤدي هذا إلى "جهنم المُرتدات" حيث تجعل الوظائف المتداخلة المتعددة مع المُرتدات الكود أكثر صعوبة في القراءة، و تصحيح الأخطاء، والتنظيم، إلخ.
async1(function (input, result1) {
async2(function (result2) {
async3(function (result3) {
async4(function (result4) {
async5(function (output) {
// القيام بشيء ما مع الإخراج
});
});
});
});
});
بالطبع، في الحياة الواقعية، سيكون هناك على الأرجح أسطر إضافية من التعليمات البرمجية للتعامل مع result1
، result2
، إلخ، وبالتالي، فإن طول وتعقيد هذه المشكلة عادةً ما ينتج عنه كود يبدو أكثر فوضوية من المثال أعلاه.
هذا هو المكان الذي تأتي فيه الوظائف للاستخدام الكبير. تتكون العمليات الأكثر تعقيدًا من العديد من الوظائف:
- أسلوب المُنشئ / الإدخال
- البرمجيات الوسيطة
- المُنهي
"أسلوب المُنشئ / الإدخال" هي الوظيفة الأولى في التسلسل. ستقبل هذه الوظيفة الإدخال الأصلي، إن وجد، للعملية. العملية هي سلسلة قابلة للتنفيذ من الوظائف، وسيكون الإدخال الأصلي في المقام الأول:
- المتغيرات في بيئة عالمية
- الاستدعاء المباشر مع أو بدون وسيطات
- القيم التي تم الحصول عليها من نظام الملفات أو طلبات الشبكة
يمكن أن تكون طلبات الشبكة طلبات واردة يبدأها شبكة أجنبية، أو تطبيق آخر على نفس الشبكة، أو التطبيق نفسه على نفس الشبكة أو شبكة أجنبية.
ستعيد دالة البرمجيات الوسيطة دالة أخرى، وستقوم دالة المُنهي باستدعاء المُرتد. يوضح ما يلي تدفق طلبات الشبكة أو نظام الملفات. هنا، يكون زمن الوصول 0 لأن كل هذه القيم متاحة في الذاكرة.
function final(someInput, callback) {
callback(`${someInput} and terminated by executing callback `);
}
function middleware(someInput, callback) {
return final(`${someInput} touched by middleware `, callback);
}
function initiate() {
const someInput = 'hello this is a function ';
middleware(someInput, function (result) {
console.log(result);
// يتطلب المُرتد `return` النتيجة
});
}
initiate();
إدارة الحالة
قد تعتمد الدوال أو لا تعتمد على الحالة. تنشأ حالة التبعية عندما يعتمد المدخل أو متغير آخر للدالة على دالة خارجية.
بهذه الطريقة ، هناك استراتيجيتان رئيسيتان لإدارة الحالة:
- تمرير المتغيرات مباشرة إلى دالة، و
- الحصول على قيمة متغير من ذاكرة تخزين مؤقتة، أو جلسة، أو ملف، أو قاعدة بيانات، أو شبكة، أو مصدر خارجي آخر.
لاحظ أنني لم أذكر المتغير العالمي. غالبًا ما تكون إدارة الحالة باستخدام المتغيرات العالمية نمطًا مضادًا غير دقيق يجعل من الصعب أو المستحيل ضمان الحالة. يجب تجنب المتغيرات العالمية في البرامج المعقدة قدر الإمكان.
تدفق التحكم
إذا كان كائن متاحًا في الذاكرة، فيمكن إجراء التكرار، ولن يكون هناك تغيير في تدفق التحكم:
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
_song += `${i} beers on the wall, you take one down and pass it around, ${
i - 1
} bottles of beer on the wall\n`;
if (i === 1) {
_song += "Hey let's get some more beer";
}
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
console.log(_song);
}
const song = getSong();
// this will work
singSong(song);
ومع ذلك، إذا كانت البيانات موجودة خارج الذاكرة، فلن يعمل التكرار بعد الآن:
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
/* eslint-disable no-loop-func */
setTimeout(function () {
_song += `${i} beers on the wall, you take one down and pass it around, ${
i - 1
} bottles of beer on the wall\n`;
if (i === 1) {
_song += "Hey let's get some more beer";
}
}, 0);
/* eslint-enable no-loop-func */
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
console.log(_song);
}
const song = getSong('beer');
// this will not work
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!
لماذا حدث هذا؟ تقوم setTimeout
بتوجيه وحدة المعالجة المركزية لتخزين التعليمات في مكان آخر على الحافلة، وتوجه إلى أن البيانات مجدولة للاستلام في وقت لاحق. تمر آلاف دورات وحدة المعالجة المركزية قبل أن تصطدم الدالة مرة أخرى عند علامة 0 ميلي ثانية، تسترد وحدة المعالجة المركزية التعليمات من الحافلة وتنفذها. المشكلة الوحيدة هي أن أغنية ('') تم إرجاعها قبل آلاف الدورات.
ينشأ نفس الموقف في التعامل مع أنظمة الملفات وطلبات الشبكة. لا يمكن ببساطة حظر مؤشر ترابط التشغيل الرئيسي لفترة زمنية غير محددة - لذلك، نستخدم وظائف الاستدعاء للجدولة تنفيذ التعليمات البرمجية في الوقت المناسب بطريقة منظمة.
ستتمكن من إجراء معظم عملياتك باستخدام أنماط الثلاثة التالية:
- على التوالي: سيتم تنفيذ الدوال بترتيب تسلسلي صارم، وهذا مشابه جدًا لحلقات
for
.
// operations defined elsewhere and ready to execute
const operations = [
{ func: function1, args: args1 },
{ func: function2, args: args2 },
{ func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
// executes function
const { args, func } = operation;
func(args, callback);
}
function serialProcedure(operation) {
if (!operation) process.exit(0); // finished
executeFunctionWithArgs(operation, function (result) {
// continue AFTER callback
serialProcedure(operations.shift());
});
}
serialProcedure(operations.shift());
متوازي بالكامل
: عندما لا تكون الترتيب مشكلة، مثل إرسال بريد إلكتروني إلى قائمة من مليون مستلم بريد إلكتروني.
let count = 0;
let success = 0;
const failed = [];
const recipients = [
{ name: 'Bart', email: 'bart@tld' },
{ name: 'Marge', email: 'marge@tld' },
{ name: 'Homer', email: 'homer@tld' },
{ name: 'Lisa', email: 'lisa@tld' },
{ name: 'Maggie', email: 'maggie@tld' },
];
function dispatch(recipient, callback) {
// `sendEmail` is a hypothetical SMTP client
sendMail(
{
subject: 'Dinner tonight',
message: 'We have lots of cabbage on the plate. You coming?',
smtp: recipient.email,
},
callback
);
}
function final(result) {
console.log(`Result: ${result.count} attempts \
& ${result.success} succeeded emails`);
if (result.failed.length)
console.log(`Failed to send to: \
\n${result.failed.join('\n')}\n`);
}
recipients.forEach(function (recipient) {
dispatch(recipient, function (err) {
if (!err) {
success += 1;
} else {
failed.push(recipient.name);
}
count += 1;
if (count === recipients.length) {
final({
count,
success,
failed,
});
}
});
});
- متوازي محدود: متوازي مع حد، مثل إرسال بريد إلكتروني ناجح لمليون مستلم من قائمة تضم 10 ملايين مستخدم.
let successCount = 0;
function final() {
console.log(`dispatched ${successCount} emails`);
console.log('finished');
}
function dispatch(recipient, callback) {
// `sendEmail` is a hypothetical SMTP client
sendMail(
{
subject: 'Dinner tonight',
message: 'We have lots of cabbage on the plate. You coming?',
smtp: recipient.email,
},
callback
);
}
function sendOneMillionEmailsOnly() {
getListOfTenMillionGreatEmails(function (err, bigList) {
if (err) throw err;
function serial(recipient) {
if (!recipient || successCount >= 1000000) return final();
dispatch(recipient, function (_err) {
if (!_err) successCount += 1;
serial(bigList.pop());
});
}
serial(bigList.pop());
});
}
sendOneMillionEmailsOnly();
لكل منها حالات استخدام وفوائد ومشاكل يمكنك تجربتها وقراءتها بمزيد من التفاصيل. والأهم من ذلك، تذكر تقسيم عملياتك إلى وحدات وتطبيق وظائف الاستدعاء! إذا شعرت بأي شك، فعامل كل شيء كما لو كان برنامجًا وسيطًا!