دليل JavaScript Async/Await: تعلّم Callbacks وPromises وAsync/Await عبر مثال متجر المثلجات

دقائق القراءة: 8
شرح البرمجة غير المتزامنة في جافاسكربت باستخدام مثال متجر المثلجات

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

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

محاور شرح البرمجة غير المتزامنة في جافاسكربت

ما المقصود بالبرمجة غير المتزامنة في JavaScript؟

مفهوم البرمجة غير المتزامنة في جافاسكربت

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

  • قراءة البيانات من واجهة برمجية API.
  • رفع الملفات.
  • الانتظار لعدة ثوانٍ قبل تنفيذ مهمة.
  • التعامل مع إدخال المستخدم.

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

الفرق بين التنفيذ المتزامن وغير المتزامن

الفرق بين التنفيذ المتزامن وغير المتزامن في جافاسكربت

ما هو النظام المتزامن؟

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

مثال بصري على التحميل المتزامن للمهام

لغة JavaScript بطبيعتها تعمل بأسلوب متزامن وضمن خيط تنفيذ واحد single-threaded، لكن هذا لا يمنعها من التعامل مع العمليات غير المتزامنة بآليات خاصة.

ما هو النظام غير المتزامن؟

في النظام غير المتزامن Asynchronous يمكن لبعض المهام أن تُدار باستقلالية دون أن تعطل بقية المهام. تخيل أن لديك عشر أيدٍ بدلًا من يد واحدة، فيمكن تنفيذ عدة أعمال في الوقت نفسه أو على الأقل دون انتظار خطّي صارم.

مثال بصري على التحميل غير المتزامن للمهام

في هذا النموذج، كل مهمة تسير وفق مدتها الخاصة، ولا تُجبِر بقية المهام على التوقف.

تلخيص سريع للفارق

ملخص بصري للتنفيذ المتزامن في جافاسكربتملخص بصري للتنفيذ غير المتزامن في جافاسكربت

  • في النظام المتزامن: تعطل مهمة واحدة قد يؤخر ما بعدها.
  • في النظام غير المتزامن: كل مهمة تتابع مسارها حسب ظروفها الخاصة.

أمثلة عملية على الكود المتزامن وغير المتزامن

أمثلة على الكود المتزامن وغير المتزامن في جافاسكربت

مثال على كود متزامن

مثال على تنفيذ متزامن في جافاسكربت

هذا المثال يوضح تنفيذ الأوامر سطرًا بعد سطر:

console.log("I");
console.log("eat");
console.log("Ice Cream");

ناتج تنفيذ الكود المتزامن في وحدة التحكم

مثال على كود غير متزامن

مثال على تنفيذ غير متزامن باستخدام setTimeout في جافاسكربت

هنا سنستخدم الدالة setTimeout() لتأخير تنفيذ جزء من الكود لمدة ثانيتين:

console.log("I");

setTimeout(() => {
  console.log("eat");
}, 2000);

console.log("Ice Cream");

لاحظ أن السطر الأخير سيظهر قبل الرسالة المؤجلة، لأن JavaScript لا تتوقف عن تنفيذ بقية الأوامر أثناء الانتظار.

ناتج تنفيذ الكود غير المتزامن في جافاسكربت

إعداد مشروع متجر المثلجات

إعداد مشروع عملي لشرح Async Await في جافاسكربت

يمكنك تنفيذ الأمثلة في CodePen أو VS Code أو أي محرر تفضله. يكفي أن تفتح ملف JavaScript وتعرض النتائج في وحدة التحكم Console.

ما هي Callbacks في JavaScript؟

شرح مفهوم callbacks في جافاسكربت

عندما تمرر دالة إلى دالة أخرى على أنها وسيط argument، فهذه تسمى callback. الفكرة ببساطة هي: نفّذ هذا الجزء من الكود لاحقًا عندما يحين الوقت المناسب.

رسم توضيحي لفكرة callback في جافاسكربت

لماذا نستخدم Callbacks؟

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

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

خطوات إعداد المثلجات في المثال العملي

تقسيم المشروع إلى مكونات

تقسيم متجر المثلجات إلى مخزن ومطبخ

  • المخزن: يحتوي على المكونات، ويمثل جانب البيانات أو Backend.
  • المطبخ: تجري فيه عملية الإنتاج، ويمكن تشبيهه بالمنطق التنفيذي أو الواجهة العملية.

تخزين البيانات داخل كائن

تخزين بيانات المكونات داخل object في جافاسكربت

let stocks = {
  Fruits: ["strawberry", "grapes", "banana", "apple"],
  liquid: ["water", "ice"],
  holder: ["cone", "cup", "stick"],
  toppings: ["chocolate", "peanuts"],
};

إنشاء دالتين: الطلب والإنتاج

إنشاء دوال الطلب والإنتاج في مثال المثلجات

let order = () => {};
let production = () => {};

سنربط الدالتين باستخدام callback:

let order = (call_production) => {
  call_production();
};

let production = () => {};

اختبار بسيط لفهم العلاقة

let order = (call_production) => {
  console.log("Order placed. Please call production");
  call_production();
};

let production = () => {
  console.log("Production has started");
};

order(production);

نتيجة اختبار callback بين order وproduction

إضافة اسم الفاكهة إلى الطلب

let order = (fruit_name, call_production) => {
  call_production();
};

let production = () => {};

order("", production);

الآن سنضيف عامل الزمن باستخدام setTimeout() لأن كل خطوة في المشروع تستغرق مدة مختلفة.

المدد الزمنية لخطوات تحضير المثلجات

let order = (fruit_name, call_production) => {
  setTimeout(function () {
    console.log(`${stocks.Fruits[fruit_name]} was selected`);
    call_production();
  }, 2000);
};

let production = () => {};

order(0, production);

نتيجة اختيار الفاكهة بعد تأخير زمني في جافاسكربت

كتابة منطق الإنتاج باستخدام تداخل setTimeout()

let production = () => {
  setTimeout(() => {
    console.log("production has started");
    setTimeout(() => {
      console.log("The fruit has been chopped");
      setTimeout(() => {
        console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} Added`);
        setTimeout(() => {
          console.log("start the machine");
          setTimeout(() => {
            console.log(`Ice cream placed on ${stocks.holder[1]}`);
            setTimeout(() => {
              console.log(`${stocks.toppings[0]} as toppings`);
              setTimeout(() => {
                console.log("serve Ice cream");
              }, 2000);
            }, 3000);
          }, 2000);
        }, 1000);
      }, 1000);
    }, 2000);
  }, 0);
};

نتيجة تنفيذ خطوات الإنتاج المتداخلة باستخدام callbacks

مشكلة Callback Hell

هذا التداخل الكبير في الدوال يجعل الكود أصعب في القراءة والصيانة. هذه المشكلة تعرف باسم Callback Hell.

توضيح مشكلة callback hell في جافاسكربت

كيف تساعد Promises في حل مشكلة Callback Hell؟

شرح promises كحل لمشكلة callback hell

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

الشكل العام لوعد Promise في جافاسكربتدورة حياة Promise في جافاسكربت

حالات Promise

  • Pending: الحالة الابتدائية، والوعد ما زال قيد التنفيذ.
  • Resolved: تم تنفيذ المهمة بنجاح.
  • Rejected: فشلت المهمة أو تعذر إكمالها.

بناء دالة order() باستخدام Promise

let is_shop_open = true;

let order = (time, work) => {
  return new Promise((resolve, reject) => {
    if (is_shop_open) {
      setTimeout(() => {
        resolve(work());
      }, time);
    } else {
      reject(console.log("Our shop is closed"));
    }
  });
};

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

اختبار أولي

order(2000, () => console.log(`${stocks.Fruits[0]} was selected`));

اختبار أولي لاستخدام promise في مثال المثلجات

ربط الوعود باستخدام .then()

يمكننا تحديد ما يجب أن يحدث بعد كل خطوة ناجحة باستخدام المعالج .then(). هذا الأسلوب يسمى Promise Chaining.

توضيح Promise chaining باستخدام then

من المهم إعادة return داخل كل .then() حتى تستمر السلسلة بشكل صحيح.

order(2000, () => console.log(`${stocks.Fruits[0]} was selected`))
  .then(() => {
    return order(0, () => console.log("production has started"));
  })
  .then(() => {
    return order(2000, () => console.log("Fruit has been chopped"));
  })
  .then(() => {
    return order(1000, () => console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`));
  })
  .then(() => {
    return order(1000, () => console.log("start the machine"));
  })
  .then(() => {
    return order(2000, () => console.log(`ice cream placed on ${stocks.holder[1]}`));
  })
  .then(() => {
    return order(3000, () => console.log(`${stocks.toppings[0]} as toppings`));
  })
  .then(() => {
    return order(2000, () => console.log("Serve Ice Cream"));
  });

نتيجة ربط الوعود then في مثال متجر المثلجات

معالجة الأخطاء باستخدام .catch()

عند فشل أي خطوة في السلسلة، نستخدم .catch() لالتقاط الخطأ.

let is_shop_open = false;
.catch(() => {
  console.log("Customer left");
})

التعامل مع الأخطاء باستخدام catch في promises

تنفيذ كود نهائي باستخدام .finally()

المعالج .finally() يُنفذ سواء نجحت العملية أو فشلت، وهو مثالي للخطوات الختامية.

.finally(() => {
  console.log("end of day");
})

استخدام finally في promises داخل جافاسكربت

كيف يعمل Async/Await في JavaScript؟

شرح async await في جافاسكربت

يعد Async/Await طريقة أكثر وضوحًا لكتابة الكود غير المتزامن المعتمد على Promises. وهو لا يلغي الوعود، بل يبني فوقها واجهة كتابة أسهل قراءة وصيانة.

تحويل الدالة إلى دالة غير متزامنة

عند وضع الكلمة المفتاحية async قبل الدالة، فإنها تصبح دالة تعيد Promise تلقائيًا.

function order() {
  return new Promise((resolve, reject) => {
    // Write code here
  });
}
async function order() {
  // Write code here
}

استخدام try وcatch

مع Async/Await نعتمد غالبًا على try لتنفيذ الكود وcatch لالتقاط الأخطاء، ويمكن أيضًا استخدام finally.

async function kitchen() {
  try {
    await abc;
  } catch (error) {
    console.log("abc does not exist", error);
  } finally {
    console.log("Runs code anyways");
  }
}

kitchen();

ما وظيفة await؟

تجعل الكلمة المفتاحية await تنفيذ الدالة الحالية ينتظر حتى تكتمل Promise، ثم تُعاد النتيجة. لكنها لا توقف التطبيق بالكامل، بل توقف فقط سياق التنفيذ داخل تلك الدالة غير المتزامنة.

مثال بسيط على await

function toppings_choice() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(console.log("which topping would you love?"));
    }, 3000);
  });
}

async function kitchen() {
  console.log("A");
  console.log("B");
  console.log("C");
  await toppings_choice();
  console.log("D");
  console.log("E");
}

kitchen();

console.log("doing the dishes");
console.log("cleaning the tables");
console.log("taking orders");

نتيجة استخدام await مع استمرار تنفيذ المهام الأخرى

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

بناء متجر المثلجات بالكامل باستخدام Async/Await

بناء المثال الكامل باستخدام async await في جافاسكربت

سننشئ دالة مساعدة لإدارة الوقت، ثم نستخدمها داخل دالة kitchen().

let is_shop_open = true;

function time(ms) {
  return new Promise((resolve, reject) => {
    if (is_shop_open) {
      setTimeout(resolve, ms);
    } else {
      reject(console.log("Shop is closed"));
    }
  });
}
async function kitchen() {
  try {
    await time(2000);
    console.log(`${stocks.Fruits[0]} was selected`);

    await time(0);
    console.log("production has started");

    await time(2000);
    console.log("fruit has been chopped");

    await time(1000);
    console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`);

    await time(1000);
    console.log("start the machine");

    await time(2000);
    console.log(`ice cream placed on ${stocks.holder[1]}`);

    await time(3000);
    console.log(`${stocks.toppings[0]} as toppings`);

    await time(2000);
    console.log("Serve Ice Cream");
  } catch (error) {
    console.log("customer left");
  } finally {
    console.log("Day ended, shop closed");
  }
}

kitchen();

النتيجة النهائية لمثال async await في متجر المثلجات

متى تستخدم Callbacks ومتى تستخدم Promises أو Async/Await؟

الأسلوب أفضل استخدام الميزة الأساسية التحدي
Callbacks المهام البسيطة جدًا مباشر وسريع يصعب تنظيمه عند التوسع
Promises تسلسل العمليات غير المتزامنة تنظيم أفضل ومعالجة أوضح للأخطاء قد يطول التسلسل مع كثرة .then()
Async/Await المشاريع الحديثة والكود المقروء وضوح كبير وبنية قريبة من الكود المتزامن يتطلب فهمًا جيدًا لـPromises

أفضل ممارسات عند التعامل مع البرمجة غير المتزامنة

  • ابدأ بفهم Promises أولًا قبل الاعتماد الكامل على Async/Await.
  • استخدم try/catch دائمًا عند وجود احتمال لفشل العملية.
  • لا تفرط في تداخل callbacks إذا كان بالإمكان استخدام حل أوضح.
  • حافظ على فصل منطق البيانات عن منطق العرض لتسهيل الصيانة.
  • استخدم أسماء دوال ومتغيرات معبرة مثل order وproduction وtime.

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

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

اترك تعليقاً

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