recent
أخبار ساخنة

فهم تكرار الأحداث في جافا سكريبت

الصفحة الرئيسية

 لقد شاهدت مؤخرًا المحادثات الرائعة لفيليب روبرتس وإيرين زيمر من JSConf EU حول حلقة حدث JavaScript ، والتي ألهمتني لقراءة مواصفات HTML5 نفسها والتأكد من فهمي لها حقًا. أعتقد الآن أنني قد فهمت جيدًا ماهية حلقة الحدث ، وأود أن ألخص ذلك هنا.

ذا كول ستاك

مكدس الاستدعاءات هو بنية بيانات تتذكر أين نحن في الكود. عندما ندخل وظيفة جديدة ، يتم دفع سياق التنفيذ إلى المكدس ، وعندما نعود من الوظيفة ، ينفجر سياق التنفيذ ، حتى نتمكن من الاستمرار في تشغيل الكود بعد استدعاء الوظيفة.

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

قائمة انتظار المهام وحلقة الأحداث

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

مثال أساسي لتوضيح كيفية عمل قائمة انتظار المهام وحلقة الحدث هو استخدام setTimeout. دعونا نلقي نظرة على الكود التالي:

console.log('Hello');
setTimeout(() => console.log('World!'), 2000);

عند تشغيل هذا الرمز ، يتم تنفيذ الخطوات التالية:

  1. يتم وضع console.log ('Hello') على مكدس المكالمات. تم تنفيذه ، وطباعة كلمة "مرحبًا" على وحدة التحكم.
  2. يتم وضع setTimeout (() => console.log ('World')، 2000) على مكدس المكالمات. يتم تنفيذ طريقة setTimeout ، والتي ترسل رد الاتصال والمؤقت إلى setTimeout API التي يوفرها المتصفح.
  3. بعد ثانيتين ، حان الوقت لتشغيل رد الاتصال () => console.log ("العالم") ، لذلك تدفعه واجهة برمجة تطبيقات الويب إلى قائمة انتظار المهام.
  4. مكدس الاستدعاءات فارغ ، لذا يمكن لحلقة الحدث إزالة رد الاتصال من قائمة انتظار المهام ودفعها إلى مكدس الاستدعاءات ، حيث يتم تشغيلها وطباعة "العالم" إلى وحدة التحكم.

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

استدعاء

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

حتى الآن ، قمنا بوصف حلقة حدث أساسية ، تتكون من قائمة انتظار مهمة واحدة وخط أنابيب للتقديم. لذا فإن المنطق يبدو كالتالي:


while(true) {
const task = queue.dequeue();
runTask(task);
if (shouldRepaint()) {
repaint();
}
}

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

من عيوب استخدام قوائم انتظار متعددة للمهام أن لديك تحكمًا أقل في توقيت الأحداث - على سبيل المثال ، إذا كان هناك قائمة انتظار للمهلة وآخر لأحداث الماوس ، يمكن أن تقرر حلقة الحدث إفراغ قائمة انتظار حدث الماوس تمامًا قبل تشغيل الأحداث في قائمة انتظار انتهاء المهلة ، مما يتسبب في تأخير أحداث المهلة أكبر من المتوقع.

الآن ، تبدو حلقة الحدث كما يلي:

while (true) {
const currentQueue = getNextQueue();
const task = currentQueue.dequeue();
runTask(task);
if (shouldRepaint()) {
repaint();
}
}

قائمة انتظار المهام الدقيقة

يمكن أن تحتوي حلقة الحدث أيضًا على قائمة انتظار للمهام الدقيقة (بشكل عام ، تعد المهام الدقيقة وعودًا) ، والتي يتم التعامل معها بعد اكتمال المهمة المختارة من قوائم الانتظار "العادية". تعد قائمة الانتظار هذه فريدة من نوعها حيث يتم إفراغها تمامًا في كل علامة من حلقة الحدث قبل أن تنتقل الحلقة إلى خط أنابيب العرض.
 
عند إضافة قائمة انتظار المهام الدقيقة إلى حلقة الحدث ، نحصل على ما يلي:
while (true) {
const currentQueue = getNextQueue();
const task = currentQueue.dequeue();
runTask(task);
while (!microtaskQueue.isEmpty()) {
const microtask = microtaskQueue.dequeue();
runTask(microtask);
}
if (shouldRepaint()) {
repaint();
}
}

 

 قائمة انتظار معاودة الاتصال بإطار الرسوم المتحركة

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

ضع كل شيء معا

هذا هو! لقد تجاوزنا جميع الأنواع المختلفة من قوائم الانتظار التي يمكن أن تحتويها حلقة الحدث ، ولدينا الآن فهم كامل لكيفية عمل حلقة الحدث. عندما نجمع كل شيء معًا ، نحصل على المنطق التالي:

while (true) {
const currentQueue = getNextQueue();
const task = currentQueue.dequeue();
runTask(task);
while (!microtaskQueue.isEmpty()) {
const microtask = microtaskQueue.dequeue();
runTask(microtask);
}
if (shouldRepaint()) {
if (!animationFrameCallbackQueue.isEmpty()) {
const animationTasks = animationFrameCallbackQueue.copyTasks();
for (task in animationTasks) {
runTask(task);
}
}
repaint();
}
}

أتمنى أن تكون قد وجدت هذا الشرح ممتعًا كما فعلت ، وسأكون سعيدًا لسماع أي تعليقات أو إحصاءات قد تكون لديكم. أراك المرة القادمة!

google-playkhamsatmostaqltradent