فهم حلقة الحدث والردود الاستدعائية والوعود و Async/Await في JavaScript

اختار المؤلف صندوق تخفيف فيروس كورونا 19 لتلقي تبرع كجزء من برنامج الكتابة من أجل التبرعات.

المقدمة

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

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

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

كمطور جافا سكريبت، تحتاج إلى معرفة كيفية العمل مع واجهات برمجة التطبيقات الويب غير المتزامنة ومعالجة الاستجابة أو الخطأ الناتج عن تلك العمليات. في هذا المقال، ستتعرف على حلقة الأحداث، والطريقة الأصلية للتعامل مع السلوك غير المتزامن من خلال التعامل مع التعهدات التي تمت إضافتها في ECMAScript 2015، والممارسة الحديثة لاستخدام async/await.

ملاحظة: يركز هذا المقال على جافا سكريبت الجانب العميل في بيئة المتصفح. تكون نفس المفاهيم عمومًا صحيحة في بيئة Node.js، ومع ذلك، يستخدم Node.js واجهات برمجة التطبيقات الخاصة به والتي تعتمد على واجهات برمجة التطبيقات C++ بدلاً من واجهات برمجة التطبيقات الويب للمتصفح. لمزيد من المعلومات حول البرمجة غير المتزامنة في Node.js، تفضل بزيارة كيفية كتابة كود غير متزامن في Node.js.

حلقة الأحداث

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

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

// قم بتعريف ثلاث دوال مثالية
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

في هذا الكود، قمت بتعريف ثلاث دوال تقوم بطباعة أرقام باستخدام console.log().

ثم، قم بكتابة استدعاء للدوال:

// قم بتنفيذ الدوال
first()
second()
third()

سيتم الإخراج بناءً على ترتيب استدعاء الدوال – first()، second()، ثم third():

Output
1 2 3

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

أضف setTimeout إلى الدالة second لتحاكي طلبًا غير متزامناً:

// قم بتعريف ثلاث دوال مثالية، ولكن واحدة منها تحتوي على كود غير متزامن
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

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

الآن قم باستدعاء الدوال، كما فعلت من قبل:

// قم بتنفيذ الدوال
first()
second()
third()

قد تتوقع أنه مع setTimeout المعد لـ 0 ثانية أن تقوم تشغيل هذه الدوال الثلاثة لا يزال سينتج في طباعة الأرقام بتتابع. ولكن بسبب أنها غير متزامنة، سيتم طباعة الدالة ذات المهلة في النهاية:

Output
1 3 2

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

الحد العلوي

الــstack، أو الكومة الفرعية، تحتوي على حالة الدالة التي تعمل حاليًا. إذا كنت غير ملم بمفهوم الكومة، يمكنك تخيلها كـمصفوفة تتبع خصائص “آخر داخل، أول خارج” (LIFO)، مما يعني أنه يمكنك فقط إضافة أو إزالة العناصر من نهاية الكومة. تقوم جافا سكريبت بتشغيل الـframe الحالي (أو استدعاء الدالة في بيئة محددة) في الكومة، ثم تقوم بإزالته والانتقال إلى الدالة التالية.

فيما يتعلق بالمثال الذي يحتوي فقط على رمز تزامني، يتولى المتصفح التنفيذ بالترتيب التالي:

  • إضافة first() إلى الكومة، تشغيل first() الذي يسجل 1 في وحدة التحكم، إزالة first() من الكومة.
  • إضافة second() إلى الكومة، تشغيل second() الذي يسجل 2 في وحدة التحكم، إزالة second() من الكومة.
  • إضافة third() إلى الكومة، تشغيل third() الذي يسجل 3 في وحدة التحكم، إزالة third() من الكومة.

المثال الثاني باستخدام setTimout يبدو مثل هذا:

  • إضافة first() إلى الكومة، تشغيل first() الذي يسجل 1 في وحدة التحكم، إزالة first() من الكومة.
  • إضافة second() إلى الكومة، تشغيل second().
    • إضافة setTimeout() إلى الكومة، تشغيل واجهة البرمجة الويبية لـ setTimeout() التي تبدأ مؤقتًا وتضيف الدالة المجهولة إلى الـqueue، ثم إزالة setTimeout() من الكومة.
  • احذف second() من الكومة.
  • أضف third() إلى الكومة، قم بتشغيل third() الذي يسجل 3 في وحدة التحكم، واحذف third() من الكومة.
  • حلقة الأحداث تفحص الطابور لأي رسائل معلقة وتجد الدالة المجهولة من setTimeout()، وتضيف الدالة إلى الكومة التي تسجل 2 في وحدة التحكم، ثم تقوم بإزالتها من الكومة.

باستخدام setTimeout، واجهة برمجة التطبيقات الويبية غير المتزامنة، يتم تقديم مفهوم الطابور، الذي ستغطيه الدورة التعليمية التالية.

الطابور

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

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

ملاحظة: هناك أيضًا قائمة انتظار أخرى تُسمى قائمة المهام أو قائمة المهام الصغيرة التي تتعامل مع الوعود. المهام الصغيرة مثل الوعود تُعالج بأولوية أعلى من المهام الكبيرة مثل setTimeout.

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

دوال الارتجاع

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

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

إليك مثالًا على كود بناء دالة عالية الترتيب ودالة استدعاء بصيغة بنائية:

// دالة
function fn() {
  console.log('Just a function')
}

// دالة تأخذ دالة أخرى كوسيطة
function higherOrderFunction(callback) {
  // عند استدعاء دالة يتم تمريرها كوسيطة، يشار إليها بأنها دالة استدعاء
  callback()
}

// تمرير دالة
higherOrderFunction(fn)

في هذا الكود، تعرّف على دالة fn، وتعرّف على دالة higherOrderFunction التي تأخذ دالة callback كوسيطة، وتمرر fn كدالة استدعاء إلى higherOrderFunction.

تشغيل هذا الكود سيعطي النتائج التالية:

Output
Just a function

لنعود إلى الدوال الأولى، الثانية، و الثالثة باستخدام setTimeout. هذا ما لديك حتى الآن:

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

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

إليك الدوال الثلاث مع تطبيق مراجع الاستدعاء:

// قم بتحديد ثلاث دوال
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // قم بتنفيذ دالة مراجع الاستدعاء
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

الآن، قم بتنفيذ الأولى و الثانية، ثم قم بتمرير الثالثة كوسيط إلى الثانية:

first()
second(third)

بعد تشغيل هذا الكود، ستحصل على الناتج التالي:

Output
1 2 3

سيتم طباعة 1 أولاً، وبعد اكتمال المؤقت (في هذه الحالة، صفر ثوانٍ، ولكن يمكنك تغييره إلى أي مدة) سيتم طباعة 2 ثم 3. عن طريق تمرير دالة كمرجع استدعاء، لقد نجحت في تأخير تنفيذ الدالة حتى يكتمل الإجراء غير المتزامن من واجهة برمجة التطبيقات عبر الويب (setTimeout).

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

الآن بعد أن تعلمت كيفية استخدام وظائف الاستدعاء للتعامل مع المهام غير المتزامنة، يشرح القسم التالي مشاكل تضمين العديد من وظائف الاستدعاء وإنشاء “هرم القدرة”.

الوظائف المتداخلة وهرم القدرة

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

إليك عرضًا لوظائف الاستدعاء المتداخلة:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

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

Output
1 2 3

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

إليك مثالًا قابلًا للتشغيل على “هرم الهلاك” الأكثر واقعية يمكنك تجربته:

// دالة غير متزامن مثالية
function asynchronousRequest(args, callback) {
  // إثارة خطأ في حال عدم تمرير الوسائط
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // إضافة رقم عشوائي فقط لكي يبدو أن الدالة الغير متزامنة المفترضة
      // قامت بإرجاع بيانات مختلفة
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// طلبات غير متزامنة متداخلة
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// تنفيذ 
callbackHell()

في هذا الكود، يجب على كل دالة أن تأخذ في الاعتبار استجابة محتملة وخطأ محتمل، مما يجعل دالة “callbackHell” مربكة بصرياً.

تشغيل هذا الكود سيمنحك النتائج التالية:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

هذه الطريقة في التعامل مع الكود غير المتزامن صعبة المتابعة. ونتيجة لذلك، تم تقديم مفهوم الوعود في ES6. وهذا هو محور القسم التالي.

الوعود

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

إنشاء وعد

يمكنك تهيئة وعد باستخدام بنية new Promise، ويجب عليك تهيئته بوظيفة. تتلقى الوظيفة التي يتم تمريرها إلى وعد معلمات resolve و reject. تتولى وظائف resolve و reject معالجة نجاح وفشل العملية، على التوالي.

اكتب السطر التالي لتعريف وعد:

// تهيئة وعد
const promise = new Promise((resolve, reject) => {})

إذا قمت بتفحص الوعد المهيأ بهذه الحالة باستخدام وحدة التحكم في المتصفح الخاص بك، ستجد أنه لديه حالة pending وقيمة undefined:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

حتى الآن، لم يتم إعداد أي شيء للوعد، لذلك سيبقى في حالة pending إلى الأبد. أول شيء يمكنك القيام به لاختبار الوعد هو إكمال الوعد بتحديده بقيمة:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

الآن، عند فحص الوعد، ستجد أن لديه حالة fulfilled، و value مضبوطة على القيمة التي قمت بتمريرها إلى resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

كما هو مذكور في بداية هذا القسم، الوعد هو كائن قد يعيد قيمة. بعد أن يتم استيفاءه بنجاح، تتغير قيمة value من undefined إلى أن تكون معبأة بالبيانات.

A promise can have three possible states: pending, fulfilled, and rejected.

  • معلق – الحالة الابتدائية قبل الاستيفاء أو الرفض
  • تم الوفاء – عملية ناجحة، تم حل الوعد
  • تم رفضه – فشلت العملية، تم رفض الوعد

بعد أن يتم تحقيقه أو رفضه، يُعتبر الوعد قد استوفي.

الآن بعد أن لديك فكرة عن كيفية إنشاء الوعود، دعنا نلقي نظرة على كيفية قد يستهلك المطورون هذه الوعود.

استهلاك الوعد

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

هكذا يمكنك إعادة القيمة وتسجيلها للوعد المثال:

promise.then((response) => {
  console.log(response)
})

الوعد الذي قمت بإنشائه كان لديه [[PromiseValue]] قيمته لقد فعلنا ذلك!. هذه القيمة هي ما سيتم تمريره إلى الدالة المجهولة باسم response:

Output
We did it!

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

الكود التالي يحاكي البيانات المرجعية التي تم إرجاعها من طلب غير متزامن كوعد:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// سجل النتيجة
promise.then((response) => {
  console.log(response)
})

استخدام بنية then يضمن أن الاستجابة response ستتم تسجيلها فقط عند اكتمال عملية setTimeout بعد 2000 ميلي ثانية. كل هذا يتم بدون تضمين الاستدعاءات الارتجالية.

الآن بعد مرور ثانيتين، سيتم حل قيمة الوعد وسيتم تسجيلها في then:

Output
Resolving an asynchronous request!

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

// ربط وعد
promise
  .then((firstResponse) => {
    // إرجاع قيمة جديدة للthen التالي
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

سيقوم الاستجابة المكتملة في الthen الثاني بتسجيل القيمة المُرجعة:

Output
Resolving an asynchronous request! And chaining!

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

معالجة الأخطاء

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

ستقوم هذه الوظيفة getUsers بتمرير علامة إلى وعد، وستعيد الوعد:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // التعامل مع resolve و reject في واجهة برمجة التطبيقات غير المتزامنة
    }, 1000)
  })
}

قم بإعداد الكود بحيث إذا كانت قيمة onSuccess تساوي true، فإن المهلة ستكتمل ببعض البيانات. إذا كانت قيمة false، فإن الوظيفة سترفض بخطأ:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // التعامل مع resolve و reject في واجهة برمجة التطبيقات غير المتزامنة
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

بالنسبة للنتيجة الناجحة، يتم إرجاع كائنات JavaScript تمثل بيانات مستخدمين عينية.

للتعامل مع الخطأ، ستستخدم الطريقة الفرعية catch. وسيتيح لك ذلك استدعاءً فاشلاً مع الـ error كمعلمة.

قم بتشغيل أمر getUser مع تعيين onSuccess إلى false، باستخدام الطريقة then لحالة النجاح والطريقة catch للخطأ:

// قم بتشغيل وظيفة getUsers مع تمييزة الكاذبة لتحفيز خطأ
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

منذ تم تشغيل الخطأ، سيتم تخطي then وسيتعامل catch مع الخطأ:

Output
Failed to fetch data!

إذا قمت بتبديل العلم واستبداله بـ resolve، فسيتم تجاهل catch، وسيتم إرجاع البيانات بدلاً من ذلك:

// قم بتشغيل دالة getUsers مع العلم الصحيح ليتم الحل بنجاح
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

سيؤدي هذا إلى الحصول على بيانات المستخدم:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

للإشارة، إليك جدول يحتوي على أساليب المعالجة على كائنات Promise:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

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

في القسم الأخير من الوعد، سيقوم هذا البرنامج التعليمي بذكر حالة استخدام شائعة لواجهة برمجة تطبيقات الويب التي تعيد الوعود: واجهة استرجاع البيانات (Fetch API).

استخدام واجهة استرجاع البيانات (Fetch API) مع الوعود

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

// استرداد مستخدم من واجهة GitHub API
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

تُرسل طلب fetch إلى عنوان URL https://api.github.com/users/octocat، والذي ينتظر بشكل غير متزامن استجابة. يمر الـ then الأول بالاستجابة إلى دالة مجهولة تقوم بتنسيق الاستجابة كـ بيانات JSON، ثم يمر بيانات JSON إلى then الثاني الذي يقوم بتسجيل البيانات في وحدة التحكم. تُسجل أي خطأ في الـ catch إلى وحدة التحكم أيضًا.

تشغيل هذا الكود سينتج عنه ما يلي:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

هذه هي البيانات المطلوبة من https://api.github.com/users/octocat، المعروضة بتنسيق JSON.

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

الوظائف الغير متزامنة باستخدام async/await

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

يمكنك إنشاء وظيفة async بإضافة الكلمة المفتاحية async قبل الوظيفة:

// إنشاء وظيفة غير متزامنة
async function getUser() {
  return {}
}

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

جرب هذا عن طريق تسجيل استدعاء للوظيفة getUser:

console.log(getUser())

سيعطيك ذلك ما يلي:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

هذا يعني أنه يمكنك معالجة وظيفة async باستخدام then بنفس الطريقة التي يمكنك بها معالجة وعد. جرب هذا باستخدام الكود التالي:

getUser().then((response) => console.log(response))

يقوم هذا الاستدعاء لـ getUser بتمرير قيمة الإرجاع إلى وظيفة مجهولة تقوم بتسجيل القيمة في وحدة التحكم.

ستتلقى ما يلي عند تشغيل هذا البرنامج:

Output
{}

يمكن للدالة async التعامل مع وعد يتم استدعاؤه داخلها باستخدام عامل await. يمكن استخدام await داخل دالة async وسينتظر حتى يتم حل الوعد قبل تنفيذ الكود المحدد.

بناءً على هذه المعرفة، يمكنك إعادة كتابة طلب الاحضار من القسم الأخير باستخدام async/await على النحو التالي:

// التعامل مع الاحضار باستخدام async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// تنفيذ الدالة الجانبية
getUser()

يضمن عوامل await هنا أن البيانات لا تُسجَّل قبل أن يقوم الطلب بملأها بالبيانات.

الآن يمكن التعامل مع البيانات النهائية داخل الدالة getUser، دون الحاجة إلى استخدام then. هذه هي نتيجة تسجيل data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

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

أخيرًا، نظرًا لأنك تعاملت بالوعد المحقق داخل الدالة الغير متزامنة، يمكنك أيضًا التعامل مع الخطأ داخل الدالة. بدلاً من استخدام طريقة catch مع then، ستستخدم نمط try/catch للتعامل مع الاستثناء.

أضف الكود المظلل التالي:

// التعامل مع النجاح والأخطاء باستخدام async/await
async function getUser() {
  try {
    // التعامل مع النجاح في try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // التعامل مع الخطأ في catch
    console.error(error)
  }
}

البرنامج سينتقل الآن إلى كتلة catch إذا حدث خطأ وسيسجل هذا الخطأ في وحدة التحكم.

يتم التعامل في الوقت الحالي مع الكود الجافا سكريبت الغير متزامن بشكل رئيسي باستخدام جملة async/await، ولكن من المهم أن يكون لديك معرفة عملية بكيفية عمل الوعود، خاصة أن الوعود قادرة على ميزات إضافية لا يمكن التعامل معها باستخدام async/await، مثل دمج الوعود مع Promise.all().

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

الختام

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

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

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript