فهم عدم التزامن في JavaScript [ترجمة Sukhjinder Arora]

مرحبا يا هبر! أقدم لكم ترجمة المقال "فهم جافا سكريبت غير المتزامن" من تأليف سوخيندر أرورا.



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

جافا سكريبت JavaScript هي لغة برمجة مفردة حيث يمكن تنفيذ شيء واحد فقط في كل مرة. وهذا هو ، في موضوع واحد ، يمكن لمحرك جافا سكريبت معالجة عبارة واحدة فقط في كل مرة.

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

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

هذا هو المكان الذي يأتي فيه التزامن JavaScript. باستخدام جافا سكريبت غير متزامن (عمليات الاسترجاع ، الوعود ، وتزامن / انتظار) ، يمكنك تنفيذ طلبات طويلة للشبكة دون حظر سلسلة الرسائل الرئيسية.

على الرغم من أنه ليس من الضروري تعلم كل هذه المفاهيم حتى تكون مطور جافا سكريبت جيدًا ، إلا أنه من المفيد التعرف عليها.

لذلك ، دون مزيد من اللغط ، دعونا نبدأ.

كيف جافا سكريبت متزامن العمل؟


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

const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 

لفهم كيفية تنفيذ التعليمات البرمجية أعلاه داخل محرك جافا سكريبت ، نحتاج إلى فهم مفهوم سياق التنفيذ ومكدس المكالمة (المعروف أيضًا باسم مكدس التنفيذ).

سياق التنفيذ


سياق التنفيذ هو مفهوم مجرد للبيئة التي يتم فيها تقييم التعليمات البرمجية وتنفيذها. كلما تم تنفيذ أي كود في JavaScript ، يتم تشغيله في سياق التنفيذ.

يتم تنفيذ رمز الوظيفة داخل سياق تنفيذ الوظيفة ، ويتم تنفيذ الرمز العالمي ، بدوره ، داخل سياق التنفيذ العام. كل وظيفة لها سياق التنفيذ الخاص بها.

استدعاء المكدس


مكدس الاستدعاءات عبارة عن مكدس له بنية LIFO (Last in، First Out، first used) ، والذي يستخدم لتخزين جميع سياقات التنفيذ التي تم إنشاؤها أثناء تنفيذ التعليمات البرمجية.

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

دعنا الآن نعود إلى مقتطف الشفرة أعلاه ومحاولة فهم كيفية أداء محرك جافا سكريبت.

 const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 



إذن ماذا حدث هنا؟


عندما بدأ تنفيذ التعليمات البرمجية ، تم إنشاء سياق تنفيذ عام (يمثل كـ main () ) وإضافته إلى الجزء العلوي من مكدس الاستدعاءات. عند مواجهة الاستدعاء إلى الوظيفة الأولى () ، تتم إضافته أيضًا إلى أعلى المكدس.

بعد ذلك ، console.log ("مرحبًا بك!") يتم وضعها في الجزء العلوي من مكدس الاستدعاءات ، بعد إزالته من المكدس. بعد ذلك ، ندعو الوظيفة الثانية () ، لذلك يتم وضعها في الجزء العلوي من المكدس.

console.log ("مرحبًا بكم هناك!") تتم إضافته إلى أعلى المكدس وتتم إزالته منه عند الانتهاء من التنفيذ. اكتمال الدالة الثانية () ، تتم إزالته أيضًا من المكدس.

console.log ("النهاية") تمت إضافته إلى أعلى المكدس وإزالته في النهاية. بعد ذلك ، تنتهي الوظيفة الأولى () وتتم إزالتها أيضًا من المكدس.

ينتهي تنفيذ البرنامج ، وبالتالي تتم إزالة سياق المكالمة العامة ( main () ) من المكدس.

كيف يعمل JavaScript غير المتزامن؟


الآن وبعد أن أصبح لدينا فهم أساسي لمجموعة مكالمات المكالمات وكيفية عمل جافا سكريبت المتزامن ، فلنعد إلى JavaScript غير المتزامن.

ما هو الحظر؟


لنفترض أننا نعالج معالجة الصور أو طلب الشبكة بشكل متزامن. على سبيل المثال:

 const processImage = (image) => { /** *    **/ console.log('Image processed'); } const networkRequest = (url) => { /** *      **/ return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting(); 

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

عند اكتمال الدالة processImage () ، تتم إزالته من المكدس. بعد ذلك ، يتم استدعاء الدالة networkRequest () وإضافتها إلى المكدس. هذا سيستغرق بعض الوقت قبل الانتهاء من التنفيذ.

في النهاية ، عندما يتم تنفيذ وظيفة networkRequest () ، يتم استدعاء وظيفة greeting () ، نظرًا لأنها تحتوي على طريقة console.log فقط ، وعادة ما يتم تشغيل هذه الطريقة بسرعة ، سيتم تنفيذ وظيفة greeting () وتنتهي على الفور.

كما ترى ، نحتاج إلى الانتظار حتى تكتمل الوظيفة (مثل processImage () أو networkRequest () ). هذا يعني أن هذه الوظائف تمنع مكدس الاستدعاءات أو الخيط الرئيسي. نتيجة لذلك ، لا يمكننا إجراء عمليات أخرى حتى يتم تنفيذ التعليمات البرمجية أعلاه.

إذن ما هو الحل؟


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

 const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); 

استخدمت هنا طريقة setTimeout لمحاكاة طلب شبكة. يرجى تذكر أن setTimeout ليس جزءًا من محرك JavaScript ، بل هو جزء من واجهة برمجة تطبيقات الويب المزعومة (في المتصفح) و C / C ++ APIs (في node.js).

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



حلقة الحدث و API على الويب وقائمة انتظار الرسائل / قائمة المهام ليست جزءًا من مشغل JavaScript ؛ فهي جزء من وقت تشغيل متصفح JavaScript أو وقت تشغيل JavaScript في Nodejs (في حالة Nodejs). في Nodejs ، يتم استبدال واجهات برمجة التطبيقات للويب بواجهة برمجة تطبيقات C / C ++.

الآن ، دعنا نعود إلى الرمز أعلاه ونرى ما يحدث في حالة التنفيذ غير المتزامن.

 const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End'); 



عندما يتم تحميل الكود أعلاه في المتصفح ، تتم إضافة console.log ("Hello World") إلى الحزمة وإزالتها منه عند الانتهاء من التنفيذ. بعد ذلك ، تتم مواجهة استدعاء إلى networkRequest () ؛ تتم إضافته إلى الجزء العلوي من المكدس.

بعد ذلك ، يتم استدعاء وظيفة setTimeout () ووضعها في الجزء العلوي من الرصة. تحتوي الدالة setTimeout () على وسيطين : 1) وظيفة رد اتصال و 2) بالمللي ثانية.

setTimeout () يبدأ مؤقت لمدة 2 ثانية في بيئة واجهة برمجة تطبيقات الويب. عند هذه النقطة ، يتم إكمال setTimeout () وإزالته من المكدس. بعد ذلك ، تتم إضافة console.log ("النهاية") إلى المكدس ، ويتم تنفيذها وإزالتها منه عند الانتهاء.

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

حلقة الحدث


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

في حالتنا ، تحتوي قائمة انتظار الرسائل على رد اتصال واحد وتكون مكدس التنفيذ فارغة. لذلك ، يضيف حلقة الحدث رد اتصال إلى أعلى المكدس.

بعد console.log ("رمز المتزامن") يضاف إلى الجزء العلوي من المكدس ، وتنفيذها وإزالتها منه. في هذه المرحلة ، يتم إكمال رد الاتصال وإزالته من المكدس ، ويكتمل البرنامج تمامًا.

أحداث دوم


تحتوي قائمة انتظار الرسائل أيضًا على عمليات رد الاتصال من أحداث DOM ، مثل النقرات وأحداث لوحة المفاتيح. على سبيل المثال:

 document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); }); 

في حالة أحداث DOM ، يحيط بمعالج الحدث واجهة برمجة تطبيقات الويب ، وينتظر حدثًا محددًا (في هذه الحالة ، نقرة) ، وعندما يحدث هذا الحدث ، يتم وضع وظيفة رد الاتصال في قائمة انتظار الرسائل ، في انتظار تنفيذها.

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

قائمة انتظار ES6 MicroTask


ملاحظة مؤلف الترجمة: في المقال ، استخدم المؤلف قائمة انتظار الرسائل / المهمة وقائمة الانتظار المهمة / الدقيقة ، ولكن إذا قمت بترجمة قائمة انتظار المهام وقائمة الانتظار الوظيفية ، فإنه من الناحية النظرية يتضح الأمر نفسه. لقد تحدثت مع مؤلف الترجمة وقررت ببساطة حذف مفهوم قائمة انتظار الوظائف. إذا كان لديك أي أفكار حول هذا ، فأنا في انتظارك في التعليقات

رابط لترجمة المقال بوعود من المؤلف نفسه


قدمت ES6 مفهوم قائمة العلامات الدقيقة ، والتي تستخدمها الوعود في جافا سكريبت. الفرق بين قائمة انتظار الرسائل وقائمة microtask هو أن قائمة انتظار microtask لها أولوية أعلى من قائمة انتظار الرسائل ، مما يعني أنه سيتم تنفيذ "الوعود" داخل قائمة انتظار microtask في وقت مبكر من عمليات رد الاتصال في قائمة انتظار الرسائل.

على سبيل المثال:

 console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End'); 

الخلاصة:

 Script start Script End Promise resolved setTimeout 

كما ترون ، تم تنفيذ "الوعد" قبل setTimeout ، كل هذا لأن استجابة "الوعد" يتم تخزينها داخل قائمة انتظار microstask ، والتي لها أولوية أعلى من قائمة انتظار الرسائل.

دعونا نلقي نظرة على المثال التالي ، هذه المرة 2 "وعود" و 2 setTimeout :

 console.log('Script start'); setTimeout(() => { console.log('setTimeout 1'); }, 0); setTimeout(() => { console.log('setTimeout 2'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End'); 

الخلاصة:

 Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2 

ومرة أخرى ، تم تنفيذ كل من "الوعود" الخاصة بنا قبل عمليات الاسترجاعات داخل setTimeout ، نظرًا لأن حلقة معالجة الأحداث تعتبر المهام من قائمة انتظار microtask أكثر أهمية من المهام من قائمة انتظار الرسائل / قائمة انتظار المهمة.

إذا ظهر أثناء تنفيذ المهام "وعد" آخر من قائمة انتظار microtask ، فسيتم إضافته إلى نهاية قائمة الانتظار هذه وتنفيذها قبل عمليات الاستعادة من قائمة انتظار الرسائل ، ولا يهم كم من الوقت ينتظرون تنفيذها.

على سبيل المثال:

 console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res)); console.log('Script End'); 

الخلاصة:

 Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout 

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

الخاتمة


لذلك ، تعلمنا كيفية عمل جافا سكريبت غير المتزامن والمفاهيم: مكدس الاستدعاء ، حلقة الحدث ، قائمة انتظار الرسائل / قائمة انتظار المهام ، وقائمة العلامات الدقيقة التي تشكل وقت تشغيل JavaScript

Source: https://habr.com/ru/post/ar439620/


All Articles