فهم حلقة الحدث وعمليات الاسترجاع

0 الأسهم
0
0
0
0

مقدمة

في بدايات الإنترنت، كانت مواقع الويب غالبًا ما تتكون من بيانات ثابتة على صفحة HTML. ولكن مع ازدياد تفاعلية وديناميكية تطبيقات الويب، أصبح من الضروري إجراء عمليات مكثفة، مثل طلبات الشبكة الخارجية لاسترجاع بيانات واجهة برمجة التطبيقات (API). لإجراء هذه العمليات في JavaScript، يجب على المطور استخدام تقنيات البرمجة غير المتزامنة.

لأن جافا سكريبت لغة برمجة أحادية الخيط، تعتمد على نموذج تنفيذ متزامن يُعالج العمليات واحدة تلو الأخرى، لذا لا يمكنها معالجة سوى أمر واحد في كل مرة. مع ذلك، قد يستغرق إجراء، مثل طلب بيانات من واجهة برمجة التطبيقات (API)، وقتًا غير محدد، حسب حجم البيانات المطلوبة، وسرعة اتصال الشبكة، وعوامل أخرى. إذا تم إجراء استدعاءات واجهة برمجة التطبيقات بشكل متزامن، فلن يتمكن المتصفح من معالجة أي مُدخلات من المستخدم، مثل التمرير أو النقر على زر، حتى اكتمال تلك العملية. يُعرف هذا بالحظر.

لتجنب سلوك الحجب، تحتوي بيئة المتصفح على العديد من واجهات برمجة تطبيقات الويب غير المتزامنة التي يمكن لجافا سكريبت الوصول إليها، أي أنه يمكن تنفيذها بالتوازي مع عمليات أخرى بدلاً من تنفيذها بالتتابع. هذا مفيد لأنه يسمح للمستخدم بمواصلة استخدام المتصفح كالمعتاد أثناء معالجة العملية غير المتزامنة.

حلقة الحدث

يشرح هذا القسم كيفية تعامل جافا سكريبت مع الشيفرة غير المتزامنة باستخدام حلقة الحدث. يبدأ بعرض توضيحي لكيفية عمل حلقة الحدث، ثم يشرح عنصرين منها: المكدس وقائمة الانتظار.

يتم تنفيذ شيفرة جافا سكريبت التي لا تستخدم أيًا من واجهات برمجة تطبيقات الويب غير المتزامنة بشكل متزامن، واحدة تلو الأخرى، بالتتابع. ويتضح ذلك من خلال هذا المثال، الذي يستدعي ثلاث دوال، كل منها تطبع رقمًا في وحدة التحكم:

// Define three example functions
function first() {
console.log(1)
}

function second() {
console.log(2)
}

function third() {
console.log(3)
}

في هذا الكود، يمكنك تعريف ثلاث وظائف تقوم بطباعة الأرقام باستخدام console.log().

ثم اكتب الاستدعاءات للوظائف:

// Execute the functions
first()
second()
third()

سيتم إنتاج الناتج بناءً على الترتيب الذي يتم به استدعاء الوظائف - first()، second()، ثم three():

Output
1
2
3

عند استخدام واجهة برمجة تطبيقات ويب غير متزامنة، تصبح القواعد أكثر تعقيدًا. إحدى واجهات برمجة التطبيقات المضمنة التي يمكنك تجربتها هي setTimeout، التي تضبط مؤقتًا وتنفذ إجراءً بعد فترة زمنية محددة. يجب أن تكون setTimeout غير متزامنة، وإلا سيتجمد المتصفح بالكامل أثناء الانتظار، مما يؤدي إلى تجربة مستخدم سيئة.

لمحاكاة طلب غير متزامن، أضف setTimeout إلى الوظيفة الثانية:

// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

تتطلب دالة setTimeout وسيطتين: دالة لتنفيذها بشكل غير متزامن، ومدة الانتظار قبل استدعاء هذه الدالة. في هذا الكود، تضع console.log في دالة مجهولة المصدر، ثم تمررها إلى setTimeout، ثم تُعيّن الدالة لتنفيذها بعد 0 ميلي ثانية.

الآن قم باستدعاء الوظائف كما في السابق:

// Execute the functions
first()
second()
third()

قد تتوقع أنه بضبط setTimeout على 0، سيظل تنفيذ هذه الدوال الثلاث يطبع الأرقام بالترتيب. ولكن نظرًا لعدم تزامنها، ستطبع الدالة مع وضع فاصل في آخرها:

Output
1
3
2

لا يهم إن كنت قد ضبطت المؤقت على صفر ثانية أو خمس دقائق - سيتم تشغيل console.log، الذي يتم استدعاؤه بواسطة شيفرة غير متزامنة، بعد الدوال المتزامنة عالية المستوى. يحدث هذا لأن بيئة جافا سكريبت المضيفة، أي المتصفح في هذه الحالة، تستخدم مفهومًا يُسمى حلقة الحدث للتعامل مع الأحداث المتزامنة أو المتوازية. بما أن جافا سكريبت لا يمكنها سوى تنفيذ أمر واحد في كل مرة، فإنها تحتاج إلى حلقة الحدث لمعرفة وقت تنفيذ أمر معين. تتعامل حلقة الحدث مع هذا باستخدام مفهومي المكدسات والطوابير.

كومة

يحتفظ المكدس، أو مكدس الاستدعاءات، بحالة الدالة قيد التنفيذ حاليًا. إذا لم تكن مُلِمًّا بمفهوم المكدس، يمكنك اعتباره مصفوفة ذات خصائص "الداخل أخيرًا، الخارج أولًا" (LIFO)، ما يعني أنه لا يمكنك إضافة أو إزالة عناصر إلا من أسفل المكدس. يُنفِّذ جافا سكريبت الإطار الحالي (أو استدعاء الدالة في بيئة مُحددة) على المكدس، ثم يُزيله وينتقل إلى الإطار التالي.

بالنسبة للمثال الذي يحتوي فقط على كود متزامن، يقوم المتصفح بالتنفيذ بالترتيب التالي:

  • أولاً() أضف إلى المكدس، ثم قم بتنفيذ first() الذي يسجل 1 في وحدة التحكم، ثم قم بإزالة first() من المكدس.
  • ثانية() أضف إلى المكدس، ثم قم بتنفيذ second() الذي يطبع 2 في وحدة التحكم، ثم قم بإزالة second() من المكدس.
  • ثالث () أضف إلى المكدس، ثم قم بتنفيذ third() الذي يسجل 3 في وحدة التحكم، ثم قم بإزالة third() من المكدس.

المثال الثاني مع setTimout هو كما يلي:

  • أولاً() أضف إلى المكدس، ثم قم بتنفيذ first() الذي يسجل 1 في وحدة التحكم، ثم قم بإزالة first() من المكدس.
  • ثانية() أضف إلى المكدس، ثم قم بتنفيذ second().
    • أضف setTimeout() إلى المكدس، وقم بتنفيذ واجهة برمجة التطبيقات على الويب setTimeout() والتي تبدأ المؤقت وتضيف الوظيفة المجهولة إلى قائمة الانتظار، ثم قم بإزالة setTimeout() من المكدس.
  • ثانية() إزالة من المكدس.
  • ثالث () أضف إلى المكدس، ثم قم بتنفيذ third() الذي يسجل 3 في وحدة التحكم، ثم قم بإزالة third() من المكدس.
  • تتحقق حلقة الحدث من قائمة الانتظار بحثًا عن أي رسائل معلقة وتجد الوظيفة المجهولة من setTimeout()، وتضيف الوظيفة إلى المكدس الذي يسجل 2 في وحدة التحكم، ثم تزيلها من المكدس.

باستخدام setTimeout، تقدم واجهة برمجة تطبيقات الويب غير المتزامنة مفهوم قائمة الانتظار، والذي سيتناوله هذا البرنامج التعليمي لاحقًا.

طابور

قائمة الانتظار، وتُسمى أيضًا قائمة انتظار الرسائل أو قائمة انتظار المهام، هي منطقة انتظار للوظائف. عندما تكون مكدس النداءات فارغًا، تتحقق حلقة الأحداث من قائمة الانتظار بحثًا عن أي رسائل معلقة، بدءًا من أقدم رسالة. عند العثور على رسالة، تُضيفها إلى المكدس، الذي يُنفذ الوظيفة الموجودة فيها.

في مثال setTimeout، تُنفَّذ الدالة المجهولة مباشرةً بعد تنفيذ المستوى الأعلى، لأن المؤقت مُعيَّن على 0 ثانية. من المهم تذكر أن المؤقت لا يعني أن الكود سيُنفَّذ في 0 ثانية بالضبط أو في أي وقت محدد، بل يعني أنه سيُضيف الدالة المجهولة إلى قائمة الانتظار خلال ذلك الوقت. يوجد نظام قائمة الانتظار هذا لأنه إذا أضاف المؤقت الدالة المجهولة مباشرةً إلى المكدس بعد انتهاء صلاحيته، فسيُقاطع الدالة قيد التنفيذ حاليًا، مما قد يُؤدِّي إلى آثار غير مقصودة وغير متوقعة.

ملاحظة: هناك أيضًا طابور آخر يُسمى طابور الوظائف أو طابور المهام الدقيقة، وهو يُعنى بالوعود. تُنفَّذ المهام الدقيقة، مثل الوعود، بأولوية أعلى من المهام الكبيرة مثل setTimeout.

الآن وقد تعرفتَ على كيفية استخدام حلقة الأحداث للمكدس وطابور التنفيذ للتعامل مع ترتيب تنفيذ الكود، فإن المهمة التالية هي معرفة كيفية التحكم في ترتيب التنفيذ في الكود. للقيام بذلك، ستتعلم أولًا الطريقة الرئيسية لضمان معالجة حلقة الأحداث للكود غير المتزامن بشكل صحيح: دوال الارتداد.

وظائف الاستدعاء

في مثال setTimeout، تُنفَّذ الدالة ذات المهلة الزمنية بعد كل شيء في السياق الرئيسي للتنفيذ عالي المستوى. ولكن للتأكد من تنفيذ إحدى الدوال، مثل الدالة الثالثة، بعد المهلة الزمنية، يجب استخدام أساليب الترميز غير المتزامنة. يمكن أن تُمثِّل المهلة الزمنية هنا استدعاءً غير متزامن لواجهة برمجة التطبيقات (API) يحتوي على بيانات. يجب العمل على البيانات الناتجة عن استدعاء واجهة برمجة التطبيقات، ولكن يجب أولًا التأكد من إرجاع البيانات.

الحل الرئيسي لهذه المشكلة هو استخدام دوال الارتداد. لا تحتوي دوال الارتداد على صيغة خاصة، بل هي ببساطة دالة تُمرَّر كمعامل إلى دالة أخرى. تُسمى الدالة التي تأخذ دالة أخرى كمعامل دالة من الدرجة الأعلى. ووفقًا لهذا التعريف، يمكن لأي دالة أن تُصبح دالة مُستدعاة إذا مُرِّرت كمعامل. المكالمات الهاتفية ليست غير متزامنة بطبيعتها، ولكن يمكن استخدامها لأغراض غير متزامنة.

فيما يلي مثال لكود بناء الجملة لوظيفة من الدرجة الأعلى واسترجاع الاتصال:

// A function
function fn() {
console.log('Just a function')
}

// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}

// Passing a function
higherOrderFunction(fn)

في هذا الكود، يمكنك تعريف دالة fn، وتعريف دالة aboveOrderFunction التي تأخذ دالة استرجاع كحجة، وتمرير fn كدالة استرجاع إلى aboveOrderFunction.

سيؤدي تشغيل هذا الكود إلى تحقيق ما يلي:

Output
Just a function

لنعد إلى الدوال الأولى والثانية والثالثة باستخدام setTimeout. إليك ما لديك حتى الآن:

function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

المهمة هي جعل الدالة الثالثة تُؤجل التنفيذ دائمًا حتى اكتمال الإجراء غير المتزامن في الدالة الثانية. وهنا يأتي دور عمليات الاستدعاء العكسي. فبدلًا من تنفيذ الأولى والثانية والثالثة في أعلى مستوى تنفيذ، تُمرر الدالة الثالثة كمُعامل إلى الثانية. تُنفذ الدالة الثانية عملية الاستدعاء العكسي بعد اكتمال الإجراء غير المتزامن.

فيما يلي ثلاث وظائف تم تنفيذها باستخدام معاودة الاتصال:

// Define three functions
function first() {
console.log(1)
}

function second(callback) {
setTimeout(() => {
console.log(2)

// Execute the callback function
callback()
}, 0)
}

function third() {
console.log(3)
}

الآن قم بتنفيذ الأول والثاني، ثم مرر الثالث كحجة إلى الثاني:

first()
second(third)

بعد تنفيذ كتلة التعليمات البرمجية هذه، ستحصل على الإخراج التالي:

Output
1
2
3

أولاً يتم طباعة 1، وبعد انتهاء المؤقت (في هذه الحالة يكون صفر ثانية، ولكن يمكنك تغييره إلى أي قيمة) تتم طباعة 2، ثم 3. اعمل حتى تكتمل واجهة برمجة التطبيقات على الويب غير المتزامنة (setTimeout).

النقطة الأساسية هنا هي أن دوال الاستدعاء ليست غير متزامنة - setTimeout هي واجهة برمجة تطبيقات ويب غير متزامنة مسؤولة عن معالجة المهام غير المتزامنة. تتيح لك دوال الاستدعاء فقط تلقي إشعار عند اكتمال مهمة غير متزامنة، ومعالجة نجاحها أو فشلها.

الآن بعد أن تعلمت كيفية استخدام عمليات الاسترجاع لأداء مهام غير متزامنة، يشرح القسم التالي المشكلات المرتبطة بتضمين عدد كبير جدًا من المكالمات وإنشاء "هرم الهلاك".

استدعاء متداخل وهرم الموت

دوال الاستدعاء طريقة فعّالة لضمان تأخير إحدى الدوال حتى اكتمال دالة أخرى وعودتها بالبيانات. ومع ذلك، نظرًا للطبيعة المتداخلة لدوال الاستدعاء، قد يصبح الكود مُشتتًا إذا كان لديك العديد من الطلبات غير المتزامنة المتتالية التي تعتمد على بعضها البعض. كان هذا مصدر إحباط كبير لمطوري جافا سكريبت الأوائل، ونتيجةً لذلك، غالبًا ما يُشار إلى الكود الذي يحتوي على استدعاءات متداخلة باسم "هرم العذاب" أو "جحيم الاستدعاء".

فيما يلي عرض توضيحي لعمليات الاسترجاع المتداخلة:

function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}

في هذا الكود، تُوضع كل دالة setTimeout جديدة داخل دالة ذات ترتيب أعلى، مما يُنشئ هرمًا من الاستدعاءات الأعمق والأعمق. سيؤدي تشغيل هذا الكود إلى إنتاج ما يلي:

Output
1
2
3

عمليًا، مع استخدام الكود غير المتزامن في العالم الحقيقي، قد يصبح هذا الأمر أكثر تعقيدًا. ستحتاج على الأرجح إلى معالجة أخطاء الكود غير المتزامن، ثم تمرير بعض البيانات من كل استجابة إلى الطلب التالي. يؤدي القيام بذلك باستخدام عمليات الاسترجاع إلى زيادة صعوبة قراءة الكود وصيانته.

فيما يلي مثال عملي لـ "هرم الموت" الأكثر واقعية والذي يمكنك اللعب به:

// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute 
callbackHell()

في هذا الكود، يجب عليك أن تأخذ بعين الاعتبار كل دالة من حيث الاستجابة المحتملة والخطأ المحتمل، مما يجعل دالة callbackHell مربكة بصريًا.

سيؤدي تشغيل هذا الكود إلى إعطائك ما يلي:

Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13

نتيجة

هذه الطريقة لإدارة الشيفرة غير المتزامنة صعبة. ونتيجةً لذلك، أُضيف مفهوم الوعود في ES6. وهذا ما سيُركز عليه القسم التالي.

تعليق واحد
اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

قد يعجبك أيضاً