
مؤلف المقال يوزع Async / Await في JavaScript باستخدام الأمثلة. بشكل عام ، يعد Async / Await طريقة ملائمة لكتابة التعليمات البرمجية غير المتزامنة. قبل هذه الفرصة ، تم كتابة رمز مشابه باستخدام عمليات الاسترجاعات والوعود. يكشف مؤلف المقال الأصلي عن فوائد Async / Await من خلال دراسة العديد من الأمثلة.
نذكرك: لجميع قراء "Habr" - خصم بقيمة 10،000 روبل عند التسجيل في أي دورة تدريبية في Skillbox باستخدام الرمز الترويجي "Habr".
توصي Skillbox بما يلي: دورة Java Developer Online التعليمية.
رد
رد الاتصال هو وظيفة تم تأخير مكالمة إلى أجل غير مسمى. في السابق ، كانت عمليات الاستدعاء تستخدم في تلك الأجزاء من الكود حيث لا يمكن الحصول على النتيجة على الفور.
فيما يلي مثال على قراءة ملف بشكل غير متزامن على Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => { if (err) { throw err; } console.log(data); });
تنشأ المشاكل عندما تحتاج إلى تنفيذ عدة عمليات غير متزامنة في آن واحد. دعونا نتخيل هذا السيناريو: تم تقديم طلب إلى قاعدة بيانات مستخدم Arfat ، تحتاج إلى قراءة حقل profile_img_url الخاص به وتنزيل صورة من خادم someserver.com.
بعد التنزيل ، قم بتحويل الصورة إلى تنسيق آخر ، على سبيل المثال ، من PNG إلى JPEG. في حالة نجاح التحويل ، يتم إرسال بريد إلكتروني إلى بريد المستخدم. علاوة على ذلك ، يتم إدخال معلومات حول الحدث في ملف transformations.log مع التاريخ.

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

كانت النقطة الإيجابية للوعود هي أن الشفرة تتم قراءتها بشكل أفضل ، من أعلى إلى أسفل ، وليس من اليسار إلى اليمين. ومع ذلك ، فإن للوعود مشاكلها:
- تحتاج إلى إضافة كمية كبيرة من. ثم.
- بدلاً من try / catch ، يتم استخدام .catch لمعالجة جميع الأخطاء.
- العمل مع العديد من الوعود خلال دورة واحدة أمر غير ملائم دائمًا ؛ وفي بعض الحالات ، يؤدي إلى تعقيد الكود.
هذه هي المهمة التي ستظهر معنى الفقرة الأخيرة.
افترض أن هناك حلقة لطباعة سلسلة من الأرقام من 0 إلى 10 بفاصل زمني عشوائي (0 - ثانية). باستخدام الوعود ، تحتاج إلى تغيير هذه الدورة بحيث يتم عرض الأرقام بالتسلسل من 0 إلى 10. لذلك ، إذا استغرق إخراج الصفر 6 ثوانٍ ووصلت الوحدات إلى ثانيتين ، فيجب أولاً إخراج الصفر ، ثم يبدأ العد التنازلي لمخرج الوحدة.
وبالطبع ، لحل هذه المشكلة ، لا نستخدم Async / Await أو .sort. مثال على الحل هو في النهاية.
وظائف المتزامن
أدت إضافة وظائف غير متزامنة إلى ES2017 (ES8) إلى تبسيط مهمة العمل مع الوعود. ألاحظ أن وظائف المزامنة تعمل على رأس الوعود. هذه الوظائف لا تمثل مفاهيم مختلفة نوعيا. تم تصور وظائف Async كبديل للرمز الذي يستخدم الوعود.
يتيح Async / Await تنظيم العمل باستخدام تعليمات برمجية غير متزامنة بأسلوب متزامن.
وبالتالي ، فإن معرفة الوعود يجعل من الأسهل فهم مبادئ Async / Await.
بناء الجملةفي الموقف المعتاد ، يتكون من كلمتين رئيسيتين: المزامنة والانتظار. الكلمة الأولى تجعل الوظيفة غير متزامنة. هذه الوظائف تسمح انتظار. في أي حال ، فإن استخدام هذه الوظيفة سوف يسبب خطأ.
يتم إدراج Async في بداية إعلان الوظيفة ، وفي حالة دالة السهم ، بين "=" علامة بين قوسين.
يمكن وضع هذه الوظائف في كائن كطرق أو استخدامها في إعلان فئة.
NB! تجدر الإشارة إلى أنه لا يمكن أن يكون مُنشئو الصفوف ورسامي الكتابة / المتراسون غير متزامنين.
دلالات وقواعد التنفيذتشبه وظائف Async بشكل أساسي وظائف JS القياسية ، ولكن هناك استثناءات.
لذلك ، ترجع وظائف المزامنة دائمًا الوعود:
async function fn() { return 'hello'; } fn().then(console.log)
على وجه الخصوص ، تقوم fn بإرجاع hello string. حسنًا ، نظرًا لأن هذه دالة غير متزامنة ، يتم لف قيمة السلسلة بوعد باستخدام المُنشئ.
هنا تصميم بديل بدون Async:
function fn() { return Promise.resolve('hello'); } fn().then(console.log);
في هذه الحالة ، تتم عودة الوعد "يدويًا". وظيفة غير متزامنة يلتف دائما في وعد جديد.
في حالة كون قيمة الإرجاع بدائية ، تُرجع الدالة غير المتزامنة قيمة ، وتلفها بوعد. في حالة كون قيمة الإرجاع هي هدف الوعد ، يتم إرجاع حلها في الوعد الجديد.
const p = Promise.resolve('hello') p instanceof Promise;
ولكن ماذا يحدث إذا حدث خطأ داخل الوظيفة غير المتزامنة؟
async function foo() { throw Error('bar'); } foo().catch(console.log);
إذا لم تتم معالجتها ، فسوف تُرجع foo () وعدًا بإعادة التقييم. في هذه الحالة ، بدلاً من Promise.resol ، سيعود Promise.reject يحتوي على خطأ.
تعطي وظائف Async في الإخراج دائمًا وعودًا ، بغض النظر عن ما يتم إرجاعه.
يتم إيقاف الوظائف غير المتزامنة في كل انتظار.
تنتظر يؤثر التعبيرات. لذلك ، إذا كان التعبير هو وعد ، يتم تعليق وظيفة المتزامن حتى يتم تنفيذ الوعد. في حالة أن التعبير ليس وعدًا ، يتم تحويله إلى وعد من خلال Promise.resolve ثم إنهائه.
فيما يلي وصف لكيفية عمل وظيفة fn.
- بعد الاتصال به ، يتم تحويل السطر الأول من const a = await 9؛ في const a = ننتظر Promise.resolve (9) ؛.
- بعد استخدام Await ، يتم تعليق تنفيذ الوظيفة حتى تتلقى قيمتها (في الوضع الحالي ، تكون 9).
- delayAndGetRandom (1000) يوقف تنفيذ الدالة fn مؤقتًا حتى ينتهي من تلقاء نفسه (بعد ثانية واحدة). هذا هو في الواقع وقف وظيفة الجبهة الوطنية لمدة 1 ثانية.
- delayAndGetRandom (1000) خلال تصميم بإرجاع قيمة عشوائية ، والتي يتم تعيينها ثم إلى المتغير ب.
- حسنًا ، حالة المتغير c تشبه حالة المتغير a. بعد ذلك ، يتوقف كل شيء لثانية واحدة ، ولكن الآن delayAndGetRandom (1000) لا يُرجع أي شيء ، لأن هذا غير مطلوب.
- نتيجة لذلك ، يتم حساب القيم بواسطة الصيغة a + b * c. يتم التفاف النتيجة في وعد باستخدام Promise.resol وإعادتها بواسطة الدالة.
قد تشبه هذه الإيقاف المؤقت المولدات في ES6 ، ولكن هناك
أسباب لذلك .
نحن نحل المشكلة
حسنًا ، لنلقِ نظرة الآن على حل المشكلة المذكورة أعلاه.

تستخدم الدالة finishMyTask Await انتظار نتائج العمليات مثل queryDatabase و sendEmail و logTaskInFile وغيرها. إذا قارنا هذا القرار بمكان استخدام الوعود ، ستظهر أوجه التشابه. ومع ذلك ، فإن الإصدار مع Async / Await يبسط إلى حد كبير جميع الصعوبات النحوية. في هذه الحالة ، لا يوجد الكثير من عمليات الاسترجاعات والسلاسل مثل .then / .catch.
هنا حل مع إخراج الأرقام ، هناك خياران.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
وهنا حل باستخدام وظائف المزامنة.
async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } }
خطأ في التعامليتم التفاف الأخطاء غير المجهزة في وعود مرفوضة. ومع ذلك ، في وظائف المزامنة ، يمكنك استخدام بنية try / catch لإجراء معالجة متزامنة للأخطاء.
async function canRejectOrReturn() {
canRejectOrReturn () هي وظيفة غير متزامنة إما تنجح ("الرقم المثالي") أو تفشل مع وجود خطأ ("عذرًا ، الرقم كبير جدًا").
async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
نظرًا لأنه من المتوقع تنفيذ canRejectOrReturn في المثال أعلاه ، فإن إنهاءه غير الناجح سوف يستلزم تنفيذ كتلة catch. نتيجة لذلك ، ستنتهي وظيفة foo إما بدون تعريف (عندما يتم إرجاع أي شيء في كتلة المحاولة) أو مع اكتشاف الخطأ. نتيجة لذلك ، لن تفشل هذه الوظيفة ، حيث أن try / catch ستتعامل مع وظيفة foo نفسها.
هنا مثال آخر:
async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } }
يجدر الانتباه إلى حقيقة أنه في المثال من foo canRejectOrReturn يتم إرجاع. فو في هذه الحالة إما يكتمل برقم مثالي أو يُرجع خطأ ("عذرًا ، العدد أكبر من اللازم"). لن يتم تنفيذ كتلة catch.
المشكلة هي أن foo ترجع الوعد الذي تم تمريره من canRejectOrReturn. لذلك ، يصبح الحل الخاص بوظيفة foo هو الحل الخاص بـ canRejectOrReturn. في هذه الحالة ، سوف يتكون الرمز من سطرين فقط:
try { const promise = canRejectOrReturn(); return promise; }
ولكن ماذا يحدث إذا استخدمت تنتظر والعودة معا:
async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
في الكود أعلاه ، تنجح foo مع اكتشاف العدد المثالي والخطأ. لن يكون هناك فشل. ولكن foo سينتهي بـ canRejectOrReturn ، وليس مع غير محدد. دعونا نتأكد من ذلك عن طريق إزالة الإرجاع في انتظار canRejectOrReturn () سطر:
try { const value = await canRejectOrReturn(); return value; }
الأخطاء الشائعة والمآزق
في بعض الحالات ، قد يؤدي استخدام Async / Await إلى حدوث أخطاء.
ننسى الانتظاريحدث هذا كثيرًا - قبل الوعد ، يتم نسيان الكلمة الرئيسية التي تنتظر الانتظار:
async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } }
في الكود ، كما ترون ، لا يوجد انتظار أو عودة. لذلك ، دائمًا ما يخرج foo بدون تعريف لمدة ثانية واحدة. لكن الوعد سوف يتحقق. إذا كان يعطي خطأ أو رفض ، فسيتم استدعاء UnhandledPromiseRejectionWarning.
وظائف المتزامن في عمليات الاسترجاعاتغالبًا ما يتم استخدام وظائف Async في .map أو .filter كـ callbacks. مثال على ذلك هو دالة fetchPublicReposCount (اسم المستخدم) ، والتي تُرجع عدد المستودعات المفتوحة على GitHub. دعنا نقول أن هناك ثلاثة مستخدمين نحتاج إلى مقاييسهم. هنا هو رمز لهذه المهمة:
const url = 'https://api.github.com/users';
نحن بحاجة إلى حسابات ArfatSalman ، octocat ، norvig. في هذه الحالة ، قم بتنفيذ:
const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; });
يجب الانتباه إلى الانتظار في .map رد الاتصال. هنا تهم مجموعة من الوعود ، حسنا .map هو رد مجهولة المصدر لكل مستخدم محدد.
استخدام متسق للغاية من الانتظارخذ الكود التالي كمثال:
async function fetchAllCounts(users) { const counts = []; for (let i = 0; i < users.length; i++) { const username = users[i]; const count = await fetchPublicReposCount(username); counts.push(count); } return counts; }
هنا ، يتم وضع رقم الريبو في متغير العد ، ثم يضاف هذا الرقم إلى مجموعة التهم. المشكلة في الكود هي أنه حتى وصول بيانات المستخدم الأولى من الخادم ، سيكون جميع المستخدمين اللاحقين في وضع الاستعداد. وبالتالي ، في لحظة واحدة ، تتم معالجة مستخدم واحد فقط.
على سبيل المثال ، إذا كانت معالجة مستخدم واحد تستغرق حوالي 300 مللي ثانية ، فبالنسبة لجميع المستخدمين ، هذه هي الثانية بالفعل ، فالوقت الذي يقضيه خطيًا يعتمد على عدد المستخدمين. ولكن نظرًا لأن الحصول على عدد وحدات إعادة الشراء لا يعتمد على بعضها البعض ، يمكن موازاة العمليات. هذا يتطلب العمل مع .map و Promise.all:
async function fetchAllCounts(users) { const promises = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); return Promise.all(promises); }
Promise.all في المدخلات يتلقى مجموعة من الوعود مع عودة الوعد. اكتمال آخر واحد بعد الانتهاء من جميع الوعود في مجموعة أو في أول عملية إعادة. قد يحدث أن جميعها لا تبدأ في نفس الوقت - من أجل ضمان التشغيل المتزامن ، يمكنك استخدام p-map.
استنتاج
ميزات Async أصبحت ذات أهمية متزايدة للتنمية. حسنًا ، من أجل الاستخدام التكيفي للوظائف غير المتزامنة ، من المفيد استخدام
متغيرات Async . يجب أن يكون مطور JavaScript على دراية جيدة بهذا.
توصي Skillbox بما يلي: