الـ async-await بديل الـ Promise ؟

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 10 دقائق

المقدمة

سنتكلم في هذه المقالة عن الـ async/await
وهو طريقة جديدة للتعامل مع الاكواد التي قد تستغرق وقتًا طويلًا
ويعد الطريقة الأكثر شيوعًا حاليًا لبساطتها وسهولتها

قبل أن نكمل عليك أولًا ان تقرأ المقالات السابقة التي تكلمنا فيها عن الـ الـ Promise، وعود الجافاسكريبت ! والـ جحيم الـ Callback

هذه المقالة سنتكلم عن مفاهيم مهمة في عالم البرمجة لان مفهوم الـ asynchronous function أو الدالة الغير متزامنة هو مفهوم مهم جدًا وشائع في عالم البرمجة
لذا سأحاول أن اعطيه حقه هنا

لكن أولًا

ما هي الـ async/await ؟

هذه مجرد keywords ستجدها في العديد من اللغات
تستخدم مع الـ Asynchronous Function
ويطلق عليها ايضًا Non-Blocking Function دالة غير مُوقِفَة

ما معنى هذه المصطلحات من الأساس ؟

لنتكلم أولًا عن الفرق بين الـ Synchronous و الـ Asynchronous

Synchronous

لنبدأ مع المفهوم المألوف لدينا وهو الـ Synchronous
وهو ببساطة أن كل سطر كود يتم تنفيذه بالترتيب
بمعنى أنه لا يمكن أن تبدأ الخطوة الثانية قبل أن تنهي الخطوة الأولى

لنتخيل أن لدينا ثلاث دوال بسيطة بهذا الشكل

function f1() {
  console.log('f1 done');
}

function f2() {
  console.log('f2 done');
}

function f3() {
  console.log('f3 done');
}

ثم قمنا باستدعاء الدوال بهذا الشكل

f1();
f2();
f3();

ناتج الطباعة سيكون بالطبع هكذا

f1 done
f2 done
f3 done

لاننا استدعينا الدالة f1 ثم f2 ثم f3 بهذا الترتيب
مستحيل أن يتم تنفيذ ما بداخل الدالة f2 قبل أن تنتهي الدالة f1 من تنفيذ ما بداخلها

فهذه تسمى عملية متزامنة كل دالة تنتظر أن تنتهي الدالة أو الكود الذي قبلها لتبدأ هي التنفيذ

بمعنى آخر عندما يصل البرنامج لتنفيذ الدالة f1 فأنه سيتوقف عن تنفيذ باقي الأسطر وباقي الدوال لحين الانتهاء من تنفيذ الدالة f1
وهذا يسمى Blocking Function بمعنى أن الدالة f1 أوقفت كل البرنامج لحين انتهاءها
وكذلك الأمر مع f2 و f3 وأي كود برمجي بشكل العام لأن الأكواد يتم تنفيذها بالترتيب أي synchronous

هل هذا الأمر مهم أن نفهمه أو له عواقب ؟، بالطبع نعم!

قبل أن نكمل دعونا نعرف الوقت المستغرق للانتهاء من تنفيذ الدوال السابقة

let start = Date.now();
function f1() {
  console.log('f1 done at :', Date.now() - start);
}

function f2() {
  console.log('f2 done at :', Date.now() - start);
}

function f3() {
  console.log('f3 done at :', Date.now() - start);
}

هنا نحن فقط نحسب الوقت التي ستستغرقه كل دالة للتنفيذ

عندما نستدعي الدوال سيكون ناتج الطباعة هكذا

f1 done at : 0
f2 done at : 0
f3 done at : 0

استغرق تنفيذ جميع الدوال 0 ميلي ثانية بسبب أنها دوال تقوم بعملية طباعة لا أكثر

ملحوظة: الأرقام التي ستراها ليست ثابتة بل تقريبية فعندما تنفذ الدوال أكثر من مرة ستجد أن الرقم يتغير أحيانًا يكون 0 أو 1 أو حتى 5
لاختلاف سرعة الجهاز أو المتصفح ستجد فروقات صغيرة جدًا في كل مرة

مشكلة الـ Synchronous

تخيل معي أن الدالة f1 تقوم بعملية ثقيلة وتستغرق وقتًا طويلًا
مهما كانت تلك العملية سواء إحضار معلومات من API او تقوم بأي اتصال بـ Server ما

لنحاكي المشكلة بمثال بسيط

let start = Date.now();
function f1() {
  let i = 0;
  // count to 1 billion
  while (i < 1000000000) {
    i += 1;
  }
  console.log('f1 done at :', Date.now() - start);
}

function f2() {
  console.log('f2 done at :', Date.now() - start);
}

function f3() {
  console.log('f3 done at :', Date.now() - start);
}

كما ترى هنا فـ f1 هنا تقوم بعملية ثقيلة بأنها تعد من صفر لمليار!

إن استدعينا الدوال بنفس الترتيب لنرى كم استغرق تنفيذ كل دالة

f1 done at : 1614
f2 done at : 1614
f3 done at : 1614

استغرق تنفيذ f1 بمفردها فقط 1614 ميلي ثانية أي ما يقارب ثانية ونصف!
ثم تم بدأ تنفيذ f1 و f2 وانتهوا في 1614 برغم بأنهما يستغرقان فقط 0 ميلي ثانية
لكنهما اضطرا لانتظار f1 لكي يبدأه

رأيت الفرق ؟ استوعبت المشكلة ؟

تخيل أن الدالة f1 هي دالة تقوم بعملية ثانوية مستقلة
والدالتين f2 و f3 لا تعتمدان على f1 بأي شكل من الأشكال

فلماذا تضطر الدالتان f2 و f3 لانتظار f1 ما دام أنهما مستقلين، وقد تضطر للانتظار دقائق وهذا وارد هل الدالتين f2 وf3 سينتظران كل هذا الوقت ؟

تخيل هذه المشكلة تواجهك في موقعك أو في مشروعك، بأن هناك دوال تجعل كامل التطبيق يتوقف أو يتآخر بدون سبب
لما تجعل شيئين لا يعتمدان على بعضهما بأن ينتظر أحدهما الآخر ؟ وبدون سبب!

هذه المشكلة تحدث عندما يكون الكود يسير بشكل synchronous
أي متزامن بأن كل سطر كل ينتظر السطر السابق أن ينتهي ليبدأ هو حتى وإن كانت الأكواد لا تعتمد على بعضها البعض

Asynchronous

هنا ظهر مفهوم Asynchronous وهو جعل جزء ما من الكود يتم تنفيذه في الخلفية دون ان يأثر في باقي أكواد التطبيق

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

ببساطة نحن نعرف أن الدالة f1 ثقيلة وتستغرق وقتًا وتجعل كامل البرنامج يتوقف لحين انتهاءها

لذا بجعل الدالة f1 تكون Asynchronous سيتم تنفيذها في الخلفية
وسيتم تنفيذ f2 و f3 على الفور بالتزامن مع تنفيذ f1
لانها سيتم تنفيذها في الخلفية

ملحوظة: في حقيقة الأمر يوجد شيء يدعى Event Loop و أشياء أخرى تحدث في الخلفية لكني لن أتطرق لها هنا بل سيتم عمل مقالة أخرى سنتكلم عن ما يحدث في الخلفية وما هي الـ Event Loop بالتفصيل وكيف تتعامل الجافاسكريبت مع تلك الأمور
وسبب وجود الـ Event Loop في الجافاسكريبت أنها One Thread
في اللغات الأخرى التي تدعم الـ Multiple Threads فأي عملية تكون Asynchronous يتم إنشاء thread خاص

دعم الـ Promise لمفهوم الـ Asynchronous

سأفترض هنا أنك قرأت المقالة الخاصة بالـ Promise لذا سأدخل في المثال بشكل مباشر

سنفترض أن هناك بيانات نريد الحصول عليها من ملف JSON وسنستخدم دالة fetch

function f1() {
  // fetch json data from a local file
  fetch('./file.json')
    .then(function (res) {
      // convert JSON object to Javascript object
      return res.json();
    })
    .then(function (data) {
      // console.log(data);
      console.log('f1 done at :', Date.now() - start);
    });
}

كما ترى هنا فالـ f1 تقوم بجلب البيانات من ملف JSON
وهي عملية قد تستغرق وقتًا غالبًا

دالة fetch تقوم بإرجاع promise
والـ promise هنا يتبع نهج الـ asynchronous
بمعنى أي دالة أو عملية promise يتم تنفيذها في الخلفية ويكمل البرنامج تنفيذ باقي الأكواد لحين انتهاءها
وعندما تنتهي الـ promise من التنفيذ يقوم البرنامج بالرجوع لها لتنفيذ الـ then الخاص بها

الآن إن استدعينا الدوال بنفس الترتيب

f1();
f2();
f3();

لنرى كم استغرق تنفيذ كل دالة

f2 done at : 0
f3 done at : 1
f1 done at : 105

لاحظ أنه تم الانتهاء f2 و f3 أولًا و f1 كانت آخر شيء
لان الـ f1 كانت promise لذا اعتبر البرنامج أنها دالة asynchronous فتركها للنهاية كما قلنا

أذكر مجددًا بأن هناك تفاصيل كثيرة تدور في الكواليس بسبب كون لغة الجافاسكريبت لغة One Thread وكيف تتعامل مع الـ Asynchronous عن طريق الـ Event Loop سنخصص مقالة نتكلم عن هذه التفاصيل

استخدام async/await بدلًا من الـ Promise

هناك طريقة جديدة للتعامل مع الـ Promise وهي الطريقة الاكثر استخداما وتداولا لبساطتها وتنظيمها وتكون مألوفة لنا بعيدا عن اسلوب الـ then الكثير

وهي طريقة الـ async/await

async function f1() {
  let res = await fetch('./file.json');
  let data = await res.json();

  // console.log(data);
  console.log('f1 done at :', Date.now() - start);
}

اظن انك بمجرد قراءتك للدالة فهمت إلى حد ما ما يحصل نحن فقط كتبنا async قبل الدالة f1 ليتم التعرف عليها كدالة غير متزامنة Asynchronous function

let res = await fetch('./file.json');

هنا يتم استقبال الـ Response Object الراجع من دالة الـ fetch في متغير res
ومعنى كلمة await انه سينتظر انتهاء دالة الـ fetch من احضار البيانات

الـ await لن تعمل الا في الدوال الغير متزامنة بمعنى انه ان لم نكتب async قبل إسم الدالة فالـ await لن تعمل

let data = await res.json();
// console.log(data);

هنا نفس الأمر سينتظر لحين الانتهاء من تحول الـ res من JSON لـ Javascript Object
ثم يخزن الناتج في متغير data ثم يطبعه إن أردنا ذلك

لاحظ أن await كأنها بديل ابسط للـ then الـ async/await جاء هنا لتبسيط شكل استعمالنا للـ Promise وتجنب كتابة شكل الـ Promises chaining الخاصة بتعدد الـ then

استخدام الـ try-catch

حسنًا، الآن في الـ promise كنا عندما يحدث خطأ كنا نستخدم دالة الـ catch للتعامل مع أي خطأ ونستطيع تفاديه

اما في الـ async/await نستخدم try-catch
نضع الأوامر داخل الـ try ونتعامل مع أي خطأ داخل الـ catch بهذا الشكل

async function f1() {
  try {
    let res = await fetch('./file.json');
    let data = await res.json();

    // console.log(data);
    console.log('f1 done at :', Date.now() - start);
  } catch (err) {
    console.log(err); // catch any errors here
  }
}

مقارنة بين كل من Callback, Promise, Async/Await

سنقوم بعمل مقارنة مرئية للفرق بين اسلوب كتابة كل من الثلاثة
ابتداءًا من مشكلة الـ Callback hell إلى الـ Promises chaining نهايتًا إلى async/await

تذكرون دالة addTwo من مقالة الـ promise ؟

function addTwo(x, y) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(x + y);
    }, 3000);
  });
}

دالة تقوم بجمع رقمين بعد ثلاث ثواني
الآن لنبدأ المقارنة

لدينا أولًا صديقنا اللدود Callback Hell

addTwo(5, 7).then(function (result) {
  addTwo(result, 10).then(function (result2) {
    addTwo(result2, 12).then(function (result3) {
      addTwo(result3, 11).then(function (result4) {
        addTwo(result4, 20).then(function (result5) {
          addTwo(result5, 32).then(function (result6) {
            addTwo(result6, 18).then(function (result7) {
              addTwo(result7, 99).then(function (result8) {
                console.log(result8); // OUTPUT: 214 (after 24 second)
              });
            });
          });
        });
      });
    });
  });
});

مازال يحتفظ بهيبته كما ترى، هرم مقلوب من 8 أدوار

ثم قمنا بجعل كل promise يقوم بإرجاع promise وانتهى بنا المطاف إلى Promises chaining

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    return addTwo(result, 12);
  })
  .then(function (result) {
    return addTwo(result, 11);
  })
  .then(function (result) {
    return addTwo(result, 20);
  })
  .then(function (result) {
    return addTwo(result, 32);
  })
  .then(function (result) {
    return addTwo(result, 18);
  })
  .then(function (result) {
    return addTwo(result, 99);
  })
  .then(function (result) {
    console.log(result); // OUTPUT: 214 (after 24 second)
  });

ولقد حصلنا حينها على مبنى من 8 طوابق

الآن كيف سيبدو شكل الكود إن استعملنا async/await دعونا نرى

async function f() {
  let result = await addTwo(5, 7);
  result = await addTwo(result, 10);
  result = await addTwo(result, 12);
  result = await addTwo(result, 11);
  result = await addTwo(result, 20);
  result = await addTwo(result, 32);
  result = await addTwo(result, 18);
  result = await addTwo(result, 99);
  console.log(result); // OUTPUT: 214 (after 24 second)
}
f(); // call f

أنظروا إلى الجمال والبساطة
أظن أن الكود أبلغ من الشرح ولا أحتاج لأقول شيء هنا
فقط نحن وضعنا الأكواد داخل دالة تدعى f لكي نستطيع استخدام الـ async/await لا أكثر

ملحوظة: قد تسمع في المستقبل عن الـ top level await وهو أن تقوم اللغة بدعم استخدام await بشكل مستقل دون أن تكون داخل دالة معمول لها async