JavaScript غير متزامن: من Callback Hell إلى Async و Await

نشرت: 2022-03-11

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

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

كانت مزامنة المهام غير المتزامنة في JavaScript مشكلة خطيرة لفترة طويلة جدًا.

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

تاريخ موجز لجافا سكريبت غير متزامن

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

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

دعنا نلقي نظرة على أمثلة كل من هذه الحلول ونفكر في تطور البرمجة غير المتزامنة في JavaScript.

للقيام بذلك ، سوف نفحص مهمة بسيطة تقوم بتنفيذ الخطوات التالية:

  1. تحقق من اسم المستخدم وكلمة المرور للمستخدم.
  2. احصل على أدوار التطبيق للمستخدم.
  3. سجل وقت وصول المستخدم إلى التطبيق.

المقاربة 1: Callback Hell (“هرم الموت”)

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

رسم توضيحي: غير متزامن نداء جافا سكريبت الجحيم المضاد للنمط

سيبدو رمز المهام الثلاث البسيطة كما يلي:

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

تحصل كل وظيفة على وسيطة وهي وظيفة أخرى يتم استدعاؤها بمعامل يمثل استجابة الإجراء السابق.

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

يصبح هذا المثال أكثر تعقيدًا بمجرد أن تدرك أن database.getRoles هي وظيفة أخرى لها عمليات رد نداء متداخلة.

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

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

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

هذا هو المكان الذي تأتي فيه وعود JavaScript الأصلية.

وعود جافا سكريبت

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

رسم توضيحي: مخطط وعود JavaScript غير متزامن

مع تطبيق الوعود ، ستبدو الشفرة في مثال JavaScript غير المتزامن كما يلي:

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

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

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

لقد قمنا بتعديل الطريقة لإرجاع Promise ، مع نداءين ، ويقوم Promise نفسه بتنفيذ إجراءات من هذه الطريقة. الآن ، سيتم تعيين resolve reject عمليات الاسترجاعات إلى طرق Promise.then و Promise.catch على التوالي.

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

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

النهج 3: عدم التزامن / انتظار

تم تخفيف هرم العذاب بشكل كبير مع إدخال الوعود. ومع ذلك ، لا يزال يتعين علينا الاعتماد على عمليات الاسترجاعات التي يتم Promise إلى .then و .catch .

مهدت الوعود الطريق لواحد من أروع التحسينات في JavaScript. جلبت ECMAScript 2017 السكر النحوي على رأس الوعود في JavaScript في شكل عبارات غير async await .

إنها تسمح لنا بكتابة رمز مستند إلى Promise كما لو كان متزامنًا ، ولكن بدون حظر مؤشر الترابط الرئيسي ، حيث يوضح نموذج الكود هذا:

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

انتظار Promise بالحل لا يُسمح به إلا في async verifyUser مما يعني أنه يجب تعريف التحقق من المستخدم باستخدام async function غير متزامنة.

ومع ذلك ، بمجرد إجراء هذا التغيير الصغير ، يمكنك await أي Promise دون إجراء تغييرات إضافية في الطرق الأخرى.

عدم التزامن - قرار الوعد الذي طال انتظاره

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

لماذا يجب أن تبدأ في استخدام وظيفة JavaScript async اليوم؟

  1. الكود الناتج هو أنظف بكثير.
  2. تعتبر معالجة الأخطاء أبسط بكثير وتعتمد على try / catch تمامًا كما هو الحال في أي رمز متزامن آخر.
  3. التصحيح أبسط بكثير. تعيين نقطة توقف داخل كتلة .then لن ينتقل إلى التالي .then لأنه يخطو فقط من خلال رمز متزامن. ولكن ، يمكنك التنقل في await المكالمات كما لو كانت مكالمات متزامنة.