كيفية استخدام Async/Await في JavaScript مع أمثلة عملية واضحة

دقائق القراءة: 9

مقدمة: لماذا نحتاج إلى Async/Await في JavaScript؟

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

في هذا الدليل، سنتدرج لفهم الأساسيات التي تسبق Async/Await، وهي Event Loop وCallbacks وPromises، ثم ننتقل إلى طريقة الاستخدام الصحيحة مع أمثلة عملية.

شرح استخدام Async Await في جافاسكربت مع أمثلة عملية للمبتدئين

ما هو Event Loop في JavaScript؟

تُعد آلية Event Loop من أهم المفاهيم لفهم طريقة عمل JavaScript. هذه اللغة تعمل بأسلوب single-threaded، أي إنها تنفذ مهمة واحدة فقط في اللحظة نفسها. ويتم ذلك عبر بنية أساسية تُعرف باسم Call Stack، حيث تدخل الأوامر للتنفيذ ثم تخرج بعد اكتمالها.

بمعنى أبسط: عندما تستدعي دالة مثل First()، تُدفع إلى Call Stack، وبعد انتهاء تنفيذها تُزال منه، ثم تنتقل المحرك إلى السطر التالي.

رسم توضيحي لآلية Event Loop وCall Stack في JavaScript

بعد انتهاء الدالة First() من التنفيذ، يتم تفريغها من Call Stack، ثم يتجه Event Loop إلى السطر التالي لوضعه في المكدس وتجهيزه للتنفيذ.

مخطط انتقال الأوامر داخل Call Stack في جافاسكربت

والنتيجة الظاهرة في وحدة التحكم ستكون كما يلي:

نتيجة تنفيذ أوامر JavaScript داخل console مع Event Loop

مثال يوضح التنفيذ غير المتزامن

console.log('First!');

setTimeout(function second() {
  console.log('Timed Out!');
}, 0);

console.log('Final!');

عند تشغيل هذا المثال، ستُطبع القيمة First! أولاً، ثم ينتقل المحرك إلى setTimeout(). ورغم أن مدة الانتظار تساوي 0، فإن الدالة لا تُنفذ فوراً، بل تُرسل إلى قائمة المهام لتُنفذ في دورة لاحقة من Event Loop.

بعد ذلك يُنفذ السطر الأخير، فتظهر Final! قبل Timed Out!. وهذا يوضح أن بعض الدوال مثل setTimeout() لا تعمل مباشرة داخل Call Stack بالطريقة التقليدية.

تُعرف هذه المهام غالباً باسم Macro Tasks، ومن أمثلتها setTimeout() وsetInterval().

توضيح ترتيب تنفيذ setTimeout داخل Event Loop في JavaScript

كيف تعمل Callbacks في JavaScript؟

دوال Callback هي ببساطة دوال تُمرر كوسيط إلى دالة أخرى ليتم استدعاؤها لاحقاً عند اكتمال مهمة محددة. كانت هذه الطريقة من أوائل أساليب التعامل مع العمليات غير المتزامنة في JavaScript.

مثال أساسي على عرض قائمة أفلام

const movies = [
  {
    title: `A New Hope`,
    body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
  },
  {
    title: `The Empire Strikes Back`,
    body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
  }
];

function getMovies() {
  setTimeout(() => {
    movies.forEach((movie, index) => {
      console.log(movie.title);
    });
  }, 1000);
}

getMovies();

في هذا المثال لدينا مصفوفة باسم movies، ودالة getMovies() مهمتها عرض عناوين الأفلام بعد تأخير بسيط.

عرض قائمة أفلام في JavaScript باستخدام setTimeout

المشكلة عند إضافة عنصر جديد بشكل غير متزامن

لنضف الآن دالة جديدة باسم createMovies() لإدراج فيلم إضافي:

const movies = [
  {
    title: `A New Hope`,
    body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
  },
  {
    title: `The Empire Strikes Back`,
    body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
  }
];

function getMovies() {
  setTimeout(() => {
    movies.forEach((movie, index) => {
      console.log(movie.title);
    });
  }, 1000);
}

function createMovies(movie) {
  setTimeout(() => {
    movies.push(movie);
  }, 2000);
}

getMovies();
createMovies({
  title: `Return of the Jedi`,
  body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
});

في هذه الحالة، لن يظهر الفيلم الثالث في وحدة التحكم. السبب أن الدالة getMovies() تنتهي بعد ثانية واحدة، بينما تحتاج createMovies() إلى ثانيتين لإضافة الفيلم الجديد. لذلك يتم عرض القائمة قبل اكتمال الإضافة.

مشكلة الترتيب الزمني عند إضافة عناصر غير متزامنة في JavaScript

حل المشكلة باستخدام Callback

يمكننا تمرير دالة getMovies() كوسيط إلى createMovies()، ثم استدعاؤها فور اكتمال الإضافة:

const movies = [
  {
    title: `A New Hope`,
    body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
  },
  {
    title: `The Empire Strikes Back`,
    body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
  }
];

function getMovies() {
  setTimeout(() => {
    movies.forEach((movie, index) => {
      console.log(movie.title);
    });
  }, 1000);
}

function createMovies(movie, callback) {
  setTimeout(() => {
    movies.push(movie);
    callback();
  }, 2000);
}

createMovies(
  {
    title: `Return of the Jedi`,
    body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
  },
  getMovies
);

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

استخدام Callback لحل مشكلة الترتيب في JavaScript

كيف تعمل Promises في JavaScript؟

تُعد Promise أسلوباً أحدث وأكثر تنظيماً للتعامل مع العمليات غير المتزامنة. وهي تمثل قيمة قد تصبح متاحة في المستقبل، أو قد تفشل العملية التي تنتجها.

للوعد البرمجي Promise ثلاث حالات أساسية:

  • قيد الانتظار pending: العملية لم تنتهِ بعد.
  • تم التنفيذ بنجاح fulfilled: تمت العملية بنجاح.
  • تم الرفض rejected: حدث خطأ أو فشلت العملية.

عند إنشاء Promise، نستقبل عادة الدالتين resolve وreject. إذا نجحت العملية نستخدم resolve()، وإذا فشلت نستخدم reject().

const movies = [
  {
    title: `A New Hope`,
    body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
  },
  {
    title: `The Empire Strikes Back`,
    body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
  }
];

function getMovies() {
  setTimeout(() => {
    movies.forEach((movie, index) => {
      console.log(movie.title);
    });
  }, 1000);
}

function createMovies(movie) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      movies.push(movie);
      const error = false;

      if (!error) {
        resolve();
      } else {
        reject('Error: Something went wrong!');
      }
    }, 2000);
  });
}

createMovies({
  title: `Return of the Jedi`,
  body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
}).then(getMovies);

بعد اكتمال Promise بنجاح، يتم استدعاء .then()، وهنا تُنفذ الدالة getMovies(). وإذا حدث خطأ، يمكن التعامل معه لاحقاً بواسطة .catch()، وهي ممارسة مهمة في التطبيقات الحقيقية.

لماذا تُعد Promises أفضل من Callbacks في كثير من الحالات؟

  • تجعل تدفق الكود أوضح وأسهل قراءة.
  • تقلل من مشكلة التداخل المفرط للدوال.
  • تدعم سلاسل التنفيذ عبر .then() و.catch().
  • تُعد الأساس الذي بُنيت عليه Async/Await.

ما هي Async/Await وكيف تعمل؟

تُستخدم Async/Await لكتابة كود غير متزامن بصياغة أقرب إلى الكود المتزامن العادي، وهذا يمنح المطور وضوحاً أكبر وسهولة أعلى في الصيانة.

عند كتابة الكلمة المفتاحية async قبل دالة، فإن هذه الدالة تُعيد دائماً Promise. أما الكلمة await فتُستخدم داخل دالة async لجعل التنفيذ ينتظر حتى يكتمل الوعد البرمجي.

async function example() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 2000);
  });

  let result = await promise;
  alert(result);
}

example();

في هذا المثال، يتوقف تنفيذ الدالة مؤقتاً عند السطر الذي يحتوي على await إلى أن يتم حل Promise. بعد مرور ثانيتين، تصبح قيمة result هي done!، ثم تُعرض للمستخدم.

الفكرة الجوهرية وراء await

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

مثال عملي على Async/Await مع قائمة الأفلام

لنحوّل المثال السابق من Promise إلى Async/Await:

const movies = [
  {
    title: `A New Hope`,
    body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
  },
  {
    title: `The Empire Strikes Back`,
    body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
  }
];

function getMovies() {
  setTimeout(() => {
    movies.forEach((movie, index) => {
      console.log(movie.title);
    });
  }, 1000);
}

function createMovies(movie) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      movies.push(movie);
      const error = false;

      if (!error) {
        resolve();
      } else {
        reject('Error: Something went wrong!');
      }
    }, 2000);
  });
}

async function init() {
  await createMovies({
    title: `Return of the Jedi`,
    body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
  });

  getMovies();
}

init();

في هذا السيناريو، تنتظر الدالة init() اكتمال createMovies() أولاً، ثم تستدعي getMovies(). النتيجة هي عرض القائمة بعد تحديثها، من دون الحاجة إلى تمرير callback أو استخدام سلسلة طويلة من .then().

متى تستخدم Async/Await؟

تكون Async/Await مناسبة جداً عندما تحتاج إلى:

  • تنفيذ طلبات API بالتسلسل.
  • قراءة بيانات أو حفظها قبل الانتقال إلى خطوة تالية.
  • كتابة كود أوضح وأسهل في المراجعة.
  • تقليل التعقيد الناتج عن تداخل Callbacks أو تعدد Promises.

أفضل الممارسات عند الاستخدام

  1. استخدم try...catch لمعالجة الأخطاء.
  2. لا تفرط في استخدام await داخل حلقات إذا كانت المهام يمكن تنفيذها بالتوازي.
  3. تأكد من أن الدالة المعنية تُعيد Promise قبل استخدام await.
  4. اجعل أسماء الدوال معبرة، مثل fetchUsers() أو saveOrder().

مثال مختصر لمعالجة الخطأ

async function init() {
  try {
    await createMovies({
      title: `Return of the Jedi`,
      body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
    });

    getMovies();
  } catch (error) {
    console.log(error);
  }
}

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

مقارنة سريعة بين Callbacks وPromises وAsync/Await

الأسلوب سهولة القراءة إدارة الأخطاء الملاءمة للمشاريع الحديثة
Callbacks متوسطة إلى ضعيفة عند التوسع أقل مرونة مناسبة للحالات البسيطة
Promises جيدة أفضل عبر .catch() شائعة جداً
Async/Await ممتازة مريحة مع try...catch الأفضل في أغلب التطبيقات الحديثة

الخلاصة التقنية

إذا أردت فهم Async/Await بشكل صحيح، فلا يكفي حفظ الصياغة فقط، بل يجب أولاً استيعاب آلية Event Loop وفكرة Callbacks ثم Promises. عملياً، تُعد Async/Await أفضل واجهة لكتابة الكود غير المتزامن في JavaScript لأنها تجعل المنطق أوضح، وتقلل الأخطاء، وتسهل صيانة المشروع مع نموه. ومع ذلك، فهي في النهاية مبنية على Promises، لذا يبقى فهم الأساس ضرورياً لأي مطور يريد كتابة كود احترافي وموثوق.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *