تصميم جافا سكريبت غير المتزامن / انتظار: نقاط القوة والمزالق وأنماط الاستخدام

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

الصورة

قدرات عدم التزامن / انتظار


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

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

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

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


تدعم جميع المتصفحات الرئيسية الوظائف غير المتزامنة ( caniuse.com )

يعني هذا المستوى من الدعم ، على سبيل المثال ، أن الرمز الذي يستخدم async / انتظار لا يحتاج إلى تبديل . بالإضافة إلى ذلك ، فإنه يسهل التصحيح ، والذي ربما يكون أكثر أهمية من عدم الحاجة إلى transpilation.

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


تصحيح وظيفة غير متزامنة. سينتظر المصحح اكتمال السطر المنتظر والانتقال إلى السطر التالي بعد اكتمال العملية

هناك قوة أخرى للآلية قيد النظر ، والتي هي أقل وضوحًا مما درسناه بالفعل ، هي وجود الكلمة الرئيسية غير async هنا. في حالتنا ، يضمن استخدامه أن القيمة التي يتم إرجاعها بواسطة getBooksByAuthorWithAwait() هي وعد. ونتيجة لذلك ، يمكنك استخدام getBooksByAuthorWithAwait().then(...) أو await getBooksByAuthorWithAwait() بناء await getBooksByAuthorWithAwait() في التعليمات البرمجية التي تستدعي هذه الوظيفة. خذ بعين الاعتبار المثال التالي (لاحظ أن هذا غير مستحسن):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

هنا يمكن وظيفة getBooksByAuthorWithPromise() ، إذا كان كل شيء على ما يرام ، أو إرجاع وعد ، أو إذا حدث خطأ - null . ونتيجة لذلك ، في حالة حدوث خطأ ، لا يمكنك الاتصال بأمان .then() . عند الإعلان عن الوظائف باستخدام async الأخطاء من هذا النوع مستحيلة.

حول سوء الفهم غير المتزامن / انتظار


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

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

ألق نظرة على getBooksByAuthorWithAwait() و getBooksByAuthorWithPromises() من المثال أعلاه. يرجى ملاحظة أنها متطابقة ليس فقط من حيث الوظيفة. لديهم أيضًا نفس الواجهات.

كل هذا يعني أنه إذا قمت باستدعاء getBooksByAuthorWithAwait() ، فسوف يعيد الوعد.

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

مطبات متزامن / انتظار


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

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

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

يبدو هذا الرمز ، من حيث المنطق ، صحيحًا. ومع ذلك ، هناك مشكلة خطيرة. هذه هي الطريقة التي تعمل بها.

  1. await bookModel.fetchAll() النظام استدعاءات await bookModel.fetchAll() وينتظر .fetchAll() الأمر .fetchAll() .
  2. بعد استلام النتيجة من bookModel.fetchAll() await authorModel.fetch(authorId) .

لاحظ أن الاستدعاء إلى authorModel.fetch(authorId) مستقل عن نتائج الاستدعاء إلى bookModel.fetchAll() ، وفي الواقع ، يمكن تنفيذ هذين الأمرين بالتوازي. ومع ذلك ، يؤدي استخدام النتائج await إلى تنفيذ هاتين المكالمتين بالتسلسل. سيكون إجمالي وقت التنفيذ المتسلسل لهذين الأمرين أطول من وقت التنفيذ المتوازي.

إليك الطريقة الصحيحة لكتابة مثل هذا الرمز:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

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

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

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

معالجة الخطأ


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

▍ محاولة / إنشاء الصيد


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

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

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

  • يمكنك معالجة الاستثناء وإرجاع القيمة العادية. إذا لم تستخدم تعبير return في catch لإرجاع ما هو متوقع بعد تنفيذ الوظيفة غير المتزامنة ، فسيكون هذا معادلاً لاستخدام أمر return undefined ؛ الأمر.
  • يمكنك ببساطة تمرير الخطأ إلى المكان الذي تم استدعاء الرمز الذي فشل فيه والسماح بمعالجته هناك. يمكنك رمي خطأ مباشرة باستخدام أمر مثل throw error; ، والذي يسمح لك باستخدام الدالة async getBooksByAuthorWithAwait() في سلسلة الوعود. أي أنه يمكن استدعاؤها باستخدام getBooksByAuthorWithAwait().then(...).catch(error => ...) . بالإضافة إلى ذلك ، يمكنك لف الخطأ في كائن Error ، والذي قد يبدو وكأنه throw new Error(error) . سيسمح ذلك ، على سبيل المثال ، عند إخراج معلومات الخطأ إلى وحدة التحكم ، اعرض مجموعة المكالمات الكاملة.
  • يمكن تمثيل الخطأ على أنه وعد مرفوض ، يبدو وكأنه return Promise.reject(error) . في هذه الحالة ، هذا يعادل أمر throw error ؛ لا ينصح بذلك.

فيما يلي فوائد استخدام بنية المحاولة / الصيد:

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

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

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

إذا قمت بتنفيذ هذا الرمز ، فسترى ReferenceError: cb is not defined رسالة خطأ ReferenceError: cb is not defined في وحدة التحكم. يتم إخراج هذه الرسالة بواسطة الأمر console.log() من catch ، وليس بواسطة JavaScript نفسها. في بعض الحالات ، تؤدي مثل هذه الأخطاء إلى عواقب وخيمة. على سبيل المثال ، في حالة استدعاء bookModel.fetchAll(); مخبأة بعمق في سلسلة من المكالمات الوظيفية وستقوم إحدى المكالمات "بابتلاع" خطأ ، وسيكون من الصعب جدًا اكتشاف مثل هذا الخطأ.

▍ إرجاع دالة لقيمتين


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

باختصار ، يمكن استخدام الوظائف غير المتزامنة ، باستخدام هذا النهج ، على النحو التالي:

 [err, user] = await to(UserModel.findById(1)); 

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

▍ استخدام الصيد


الطريقة الأخيرة لمعالجة الأخطاء ، والتي سنتحدث عنها ، هي استخدام .catch() .

فكر في كيفية عمل await . أي أن استخدام هذه الكلمة الرئيسية يجعل النظام ينتظر حتى ينتهي الوعد من عمله. وتذكر أيضًا أن أمرًا من النموذج promise.catch() يُرجع أيضًا وعدًا. يشير كل هذا إلى أنه يمكن معالجة أخطاء الوظائف غير المتزامنة على النحو التالي:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

ميزتان صغيرتان مميزتان لهذا النهج:

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

الملخص


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

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

أعزائي القراء! هل تستخدم بنية غير متزامن / انتظار في جافا سكريبت؟ إذا كان الأمر كذلك ، فيرجى إخبارنا بكيفية معالجة الأخطاء في التعليمات البرمجية غير المتزامنة.

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


All Articles