حتى الأطفال سوف يفهمون: شرح بسيط للتزامن / الانتظار والوعود في JavaScript

مرحبا يا هبر! أقدم إليكم ترجمة المقال "JavaScript Async / Await and Promises: Explated like you you five years" by Jack Pordi.

أي شخص يعتبر نفسه مطور جافا سكريبت ، في مرحلة ما ، يجب أن يكون واجه وظائف رد الاتصال ، أو الوعود ، أو في الآونة الأخيرة ، بناء جملة async / انتظار. إذا كنت في اللعبة لفترة كافية ، فمن المحتمل أنك واجهت أوقاتًا كانت وظائف رد الاتصال المتداخلة هي الطريقة الوحيدة لتحقيق عدم التزامن في JavaScript.

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

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

ماذا يعني غير متزامن؟


تظهر الصورة شخص يفكر

كقاعدة عامة ، طرح هذا السؤال ، يمكنك سماع شيء مما يلي:

  • هناك العديد من مؤشرات الترابط التي تنفذ التعليمات البرمجية في نفس الوقت.
  • يتم تنفيذ أكثر من قطعة واحدة من التعليمات البرمجية في وقت واحد.
  • هذا هو التزامن.

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

مثال الحياة


تظهر الصورة الخضار وسكين المطبخ

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

  1. يقطع الجزر.
  2. يقطع البصل.
  3. أضف الماء إلى المقلاة ، ثم قم بتشغيل الموقد وانتظر حتى يغلي.
  4. يُضاف الجزر إلى المقلاة ويترك لمدة 5 دقائق.
  5. نضيف البصل إلى المقلاة ونطهو لمدة 10 دقائق أخرى.

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

  • لا تتطلب منك الخطوات 3 و 4 و 5 فعلاً كطاهي فعل أي شيء باستثناء مراقبة العملية وتتبع الوقت.
  • تتطلب منك الخطوات 1 و 2 القيام بشيء نشط.

لذلك ، قد تكون الوصفة لطهي أكثر خبرة على النحو التالي:

  1. البدء في غليان وعاء من الماء.
  2. أثناء انتظار الغليان ، ابدأ بقطع الجزر.
  3. عندما تنتهي من تقطيع الجزر ، يجب أن يغلي الماء ، لذا أضف الجزر.
  4. بينما ينضج الجزر في مقلاة ، يقطع البصل.
  5. يُضاف البصل ويُطهى لمدة 10 دقائق أخرى.

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

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

الآن دعنا نصل إلى JavaScript ، حسناً؟


لذلك ، باتباع نفس مثال حساء الخضار ، سأكتب بعض الوظائف لتمثيل خطوات الوصفة الموصوفة أعلاه.

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

 function chopCarrots() { /*   ... */ console.log(" !"); } function chopOnions() { /*   ... */ console.log(" !"); } function addOnions() { console.log("   !"); } function addCarrots() { console.log("   !"); } 

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

للحصول على وظيفة لإرجاع وعد ، يمكنك:

  • أعد الوعد صراحةً ، أي return new Promise(…) ؛
  • إرجاع الوعد ضمنيًا - أضف async إلى إعلان الوظيفة ، أي async function foo() ؛
  • استخدام كلا الخيارين .

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

لذلك ، وظائفنا غير المتزامنة ، التي تمثل الخطوات من 3-5 لتحضير حساء الخضار ، هي كما يلي:

 async function letPotKeepBoiling(time) { return; //  ,      } async function boilPot() { return; //  ,       } 

مرة أخرى ، قمت بحذف تفاصيل التنفيذ حتى لا يتم صرفها ، لكن تم نشرها في نهاية المقالة [1].

من المهم معرفة أنه من أجل انتظار نتيجة الوعد ، بحيث يمكنك فيما بعد القيام بشيء ما ، يمكنك ببساطة استخدام الكلمة الرئيسية التي await :

 async function asyncFunction() { /*  ... */ } result = await asyncFunction(); 

لذا ، نحن الآن بحاجة فقط إلى تجميعها جميعًا:

 function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

لكن انتظر! هذا لا يعمل! سترى SyntaxError: await is only valid in async functions خطأ SyntaxError: await is only valid in async functions . لماذا؟ لأنك إذا لم تعلن عن وظيفة باستخدام async ، فسوف تُعرّف JavaScript افتراضيًا بأنها وظيفة متزامنة - ولا يعني التزامن الانتظار! [3]. هذا يعني أيضًا أنه لا يمكنك استخدام await خارج الوظيفة.

لذلك ، نضيف ببساطة async إلى وظيفة makeSoup :

 async function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

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

ماذا يحدث أثناء await المكالمات؟ حسنا ، لا شيء ... نوع من ...

في سياق وظيفة makeSoup يمكنك ببساطة التفكير في الأمر كما تتوقع حدوث شيء ما (أو نتيجة ستعود في النهاية).

لكن تذكر: أنت (مثل المعالج) لن ترغب أبدًا في الجلوس هناك وانتظر شيء ما ، بينما يمكنك قضاء وقتك في أشياء أخرى .

لذلك ، بدلاً من مجرد طهي الحساء ، يمكننا طهي شيء آخر بشكل متوازٍ:

 makeSoup(); makePasta(); 

بينما نحن في انتظار letPotKeepBoiling ، يمكننا ، على سبيل المثال ، طهي المعكرونة.

ترى؟ بناء جملة async / await سهل الاستخدام بالفعل ، إذا فهمت ذلك ، فهل توافق؟

ماذا عن الوعود العلنية؟


حسنًا ، إذا أصررت على ذلك ، فسوف أنتقل إلى استخدام الوعود الصريحة (ترجمة تعليق.: من خلال الوعود الصريحة ، يشير المؤلف مباشرةً إلى بناء جملة الوعود ، وعن طريق الوعود الضمنية ، بناء الجملة المتزامن / الانتظار ، لأنه يُرجع الوعد ضمنيًا - لا حاجة إلى الكتابة return new Promise(…) ). ضع في اعتبارك أن أساليب المزامنة / الانتظار تعتمد على الوعود بأنفسهم وبالتالي فإن كلا الخيارين متوافق تمامًا .

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

استخدم آلة وقتنا للانتقال إلى جحيم رد الاتصال [4]:

 function callbackHell() { boilPot( () => { addCarrots(); letPotKeepBoiling(() => { addOnions(); letPotKeepBoiling(() => { console.log("   !"); }, 1000); }, 5000); }, 5000, chopCarrots(), chopOnions() ); } 

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

وكما وعدنا ، نفس المثال مع وعود صريحة:

 function makeSoup() { return Promise.all([ new Promise((reject, resolve) => { chopCarrots(); chopOnions(); resolve(); }), boilPot() ]) .then(() => { addCarrots(); return letPotKeepBoiling(5); }) .then(() => { addOnions(); return letPotKeepBoiling(10); }) .then(() => { console.log("   !"); }); } 

كما ترى ، لا تزال الوعود تشبه وظائف رد الاتصال.
لن أخوض في التفاصيل ، لكن الأهم من ذلك:

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

استنتاج


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

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

ومع ذلك ، فأنا شخصياً 100٪ في مخيم async / await ، حيث أن الشفرة بالنسبة لي أكثر قابلية للفهم وتعكس بشكل أفضل تعدد المهام الحقيقي للبرمجة غير المتزامنة.



[1] : شفرة المصدر الكاملة متاحة هنا .
[2] : المقال الأصلي "وظيفة المتزامن مقابل دالة تُرجع وعدًا ، وترجمة المقال "الفرق بين دالة غير متزامنة ووظيفة تُرجع وعدًا . "
[3] : يمكنك القول إن جافا سكريبت يمكنها تحديد نوع المزامنة / الانتظار من مجموعة الوظائف والتحقق بشكل متكرر ، ولكن لم يتم تصميم JavaScript لرعاية سلامة النوع الثابت في وقت الترجمة ، ناهيك إنه أكثر ملاءمة للمطورين لمعرفة نوع الوظيفة بوضوح.
[4] : كتبت وظائف "غير متزامنة" ، بافتراض أنها تعمل تحت نفس الواجهة مثل setTimeout . لاحظ أن عمليات الاسترجاعات غير متوافقة مع الوعود والعكس.

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


All Articles