فهم Async / Await في JavaScript مع أمثلة

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

نذكر: لجميع قراء "Habr" - خصم 10 روبل عند التسجيل في أي دورة Skillbox باستخدام رمز "Habr" الترويجي.

يوصي Skillbox بما يلي: دورة تعليمية عبر الإنترنت "مطور جافا".

رد الاتصال

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

فيما يلي مثال على قراءة ملف بشكل غير متزامن في 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 مع التاريخ.

يجدر الانتباه إلى عمليات الاسترجاعات المتداخلة والعدد الكبير من}) في الجزء الأخير من الكود. يطلق عليه Callback Hell أو Pyramid of Doom.

عيوب هذه الطريقة واضحة:

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

تمت إضافة وعود إلى JavaScript لحل هذه المشكلة. إنها تسمح لك باستبدال التداخل العميق لعمليات الاسترجاعات بكلمة.

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

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

هذه مهمة ستُظهر قيمة العنصر الأخير.

افترض أن لديك حلقة for تنتج سلسلة من الأرقام من 0 إلى 10 في فاصل زمني عشوائي (0-n ثانية). باستخدام الوعود ، تحتاج إلى تغيير هذه الحلقة بحيث يتم عرض الأرقام بالتسلسل من 0 إلى 10. لذا ، إذا كان ناتج الصفر يستغرق 6 ثوانٍ ، والوحدات - ثانيتان ، يجب إخراج الصفر أولاً ، ثم العد من خرج واحد سيبدأ.

وبالطبع ، لا نستخدم Async / Await أو .sort لحل هذه المشكلة. مثال على الحل في النهاية.

وظائف غير متزامنة

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

يتيح Async / Await تنظيم العمل باستخدام التعليمات البرمجية غير المتزامنة بأسلوب متزامن.

وبالتالي ، فإن معرفة الوعود تجعل من السهل فهم مبادئ Async / Await.

بناء الجملة

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

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

يتم إدخال Async في بداية إعلان الوظيفة ، وفي حالة وظيفة السهم ، بين علامة "=" والأقواس.

يمكن وضع هذه الوظائف على كائن كطرق ، أو يمكن استخدامها في إعلان فئة.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

ملحوظة! يجدر بنا أن نتذكر أن مُنشئي الفئات ومُحَصِّميها لا يمكن أن يكونوا غير متزامنين.

دلالات وقواعد التنفيذ

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

لذا ، فإن الدوال غير المتزامنة تُعيد الوعود دائمًا:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

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

هنا بناء بديل بدون Async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

في هذه الحالة ، يتم إرجاع الوعد "يدويًا". دائمًا ما يتم تغليف الوظيفة غير المتزامنة بوعد جديد.

في حال كانت القيمة المرجعة بدائية ، ترجع الدالة غير المتزامنة القيمة ، وتلفها في وعد. في حال كانت القيمة المُعادة هي كائن وعد ، يتم إرجاع حلها بوعد جديد.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

ولكن ماذا يحدث إذا كان هناك خطأ داخل الوظيفة غير المتزامنة؟

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

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

تقوم الدالات غير المتزامنة دائمًا بإرجاع الوعد ، بغض النظر عن ما يتم إرجاعه.

يتم تعليق الوظائف غير المتزامنة في كل انتظار.

الانتظار يؤثر على التعبيرات. لذلك ، إذا كان التعبير عبارة عن وعد ، يتم تعليق الدالة غير المتزامنة حتى يتم الوفاء بالوعد. إذا لم يكن التعبير وعدًا ، يتم تحويله إلى وعد عبر Promise.resolve ثم يتم إنهاؤه.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

وهنا وصف لكيفية عمل وظيفة fn.

  • بعد استدعائه ، يتم تحويل السطر الأول من const a = wait 9 ؛ في const a = wait Promise.resolve (9) ؛.
  • بعد استخدام Await ، يتم تعليق تنفيذ الوظيفة حتى تحصل a على قيمتها (في الوضع الحالي هي 9).
  • يقوم delayAndGetRandom (1000) بإيقاف وظيفة fn مؤقتًا حتى تنتهي من تلقاء نفسها (بعد ثانية واحدة). يؤدي هذا إلى إيقاف وظيفة fn بشكل فعال لمدة ثانية واحدة.
  • يُرجع delayAndGetRandom (1000) من خلال الحل قيمة عشوائية يتم تعيينها بعد ذلك إلى b.
  • حسنًا ، الحالة مع المتغير c تشبه حالة المتغير a. بعد ذلك يتوقف كل شيء لمدة ثانية ، ولكن الآن لا يعود برنامج "delayAndGetRandom" (1000) أي شيء لأنه غير ضروري.
  • نتيجة لذلك ، يتم حساب القيم وفقًا للصيغة أ + ب * ج. يتم تغليف النتيجة بوعد باستخدام Promise.resolve وإعادتها بواسطة الوظيفة.

قد تكون هذه التوقفات المؤقتة مماثلة للمولدات في ES6 ، ولكن هذا واحد أسبابك.

نحن نحل المشكلة

حسنًا ، لنلقِ نظرة الآن على حل المشكلة المشار إليه أعلاه.

تستخدم الدالة finishMyTask انتظار انتظار نتائج العمليات مثل queryDatabase و sendEmail و logTaskInFile وغيرها. إذا قارنا هذا الحل مع الحل الذي استخدمت فيه الوعود ، يصبح التشابه واضحًا. ومع ذلك ، فإن إصدار Async / Await يبسط التعقيدات النحوية إلى حد ما. في هذه الحالة ، لا يوجد الكثير من عمليات الاسترجاعات والسلاسل مثل .then / .catch.

هنا حل مع إخراج الأرقام ، وهناك خياران هنا.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

وهنا حل باستخدام الدوال غير المتزامنة.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

معالجة الخطأ

يتم تغليف الأخطاء غير المعالجة بوعد مرفوض. ومع ذلك ، يمكنك استخدام بنية try / catch في وظائف غير متزامنة لإجراء معالجة متزامنة للأخطاء.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn () هي وظيفة غير متزامنة إما أن تنجح ("الرقم المثالي") أو تفشل مع وجود خطأ ("عذرًا ، الرقم كبير جدًا").

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

نظرًا لأن المثال أعلاه يتوقع canRejectOrReturn للتنفيذ ، فإن الفشل الأصلي سيؤدي إلى تنفيذ كتلة catch. نتيجة لذلك ، سينتهي foo إما بـ undefined (عندما لا يتم إرجاع أي شيء في كتلة try) أو مع اكتشاف خطأ. نتيجة لذلك ، لن تفشل هذه الوظيفة ، لأن try / catch ستعالج وظيفة foo نفسها.

وهنا مثال آخر:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

يجدر الانتباه إلى حقيقة أن canRejectOrReturn عاد من foo في المثال. Foo في هذه الحالة إما أن يكتمل برقم كامل أو يعرض خطأ ("عذرًا ، الرقم كبير جدًا"). لن يتم تنفيذ كتلة 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 ، وليس undefined. دعنا نتحقق من ذلك عن طريق إزالة سطر canRejectOrReturn () العائد:

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

الأخطاء والمزالق الشائعة

في بعض الحالات ، قد يؤدي استخدام Async / Await إلى حدوث أخطاء.

نسي الانتظار

يحدث هذا كثيرًا - يتم نسيان الكلمة الرئيسية المنتظرة قبل الوعد:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

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

وظائف غير متزامن في عمليات الاسترجاعات

غالبًا ما تُستخدم الدوال غير المتزامنة في .map أو .filter كعناصر نداء. مثال على ذلك هو دالة fetchPublicReposCount (اسم المستخدم) ، والتي تُرجع عدد المستودعات المفتوحة على GitHub. لنفترض أن هناك ثلاثة مستخدمين نريد مقاييسهم. إليك رمز هذه المهمة:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

نحتاج إلى حسابات ArfatSalman و octocat و norvig. في هذه الحالة ننفذ:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

لاحظ في انتظار رد نداء خريطة. التهم هنا عبارة عن مجموعة من الوعود ، والخريطة هي رد اتصال مجهول لكل مستخدم محدد.

استخدام متسق للغاية للانتظار

لنأخذ هذا الرمز كمثال:

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;
}

هنا ، يتم وضع عدد repos في متغير count ، ثم يتم إضافة هذا الرقم إلى صفيف 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.

اختتام

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

يوصي Skillbox بما يلي:

المصدر: www.habr.com

إضافة تعليق