التعامل مع أكثر من Promise في آن واحد

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

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

المقدمة

شرحنا في مقالة سابقة عن الـ Promise وكيف نتعامل معها
وكنا نشرح كيف نتعامل مع Promise واحد فقط كل مرة
لكن احيانا سنحتاج لنتعامل مع أكثر من Promise في آن واحد

لذا سنركز في هذه المقالة البسيطة في شرح كيفية التعامل مع أكثر من Promise في آن واحد
بطرق مختلفة سواء كـ Sequential أو Concurrency أو Parallelism

سأقوم فقط بشرح كل الطرق الممكنة التي تستطيع من خلالها التعامل مع أكثر من promise في آن واحد
لن اتعمق كثيرًا في هذه الطرق، بل فقط سأعرضها عليك وكيف تستخدمها بشكل مبسط

انصحك بقراءة هذه المقالة sequential-concurrency-parallelism لتستوعب الفروق بينهم بشكل أفضل حين نتكلم عنهم

ماذا لدينا هنا ؟

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

    console.log(data);
  } catch (err) {
    console.log(err);
  }
}

هنا نقوم بعمل عملية واحدة فقط لذا كان لدينا Promise واحد فقط، الأمر بسيط كما ترى
لكن إن أردنا أن نقوم بطلب أكثر من شيء من الـ Server في نفس اللحظة!

فمثلا موقع مثل LinkedIn عندما تذهب للصفحة الرئيسية يتم عرض لك الكثير من الأشياء
مثل بياناتك الشخصية والمنشورات التي نشرها الآخرون
ويتم عرض لك مقترحات لأشخاص لكي تتابعهم أو اعلانات ترويجية لشيء ما
وأمور كثيرة يتم عرضها لك، لا يهم ما هى بالتحديد
المهم أن الموقع يقوم هنا بعمل أكثر من طلب للـ Server

سؤال هل هذه العمليات مرتبطة ببعض ؟ الاجابة لا
لذا هذه عمليات مستقلة تمامًا ليست مرتبطة ببعضهما البعض
ويجب عرضها بشكل متزامن دون أن تأثر واحدة على الأخرى

لو كانت الإجابة نعم فيمكنك الاستمرار في ما تفعله، في النهاية الأمر يعود لك
لكن سنفترض هنا أن هذه مشكلة ونريد حلها

تجهيز قاعدة بيانات افتراضية

سنحاول محاكاة المثال السابق بشكل عملي
سنقوم أولًا بعمل قاعدة بيانات بسيطة لكل من users, posts, ads
فسنفترض أن لدينا ملف json يدعى users.json يمثل الأشخاص بهذا الشكل

{
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]"
    }
  ]
}

وملف يدعى posts.json يمثل المنشورات

{
  "posts": [
    {
      "id": 1,
      "userId": 1,
      "title": "title 1",
      "content": "content 1"
    },
    {
      "id": 2,
      "userId": 2,
      "title": "title 2",
      "content": "content 2"
    },
    {
      "id": 3,
      "userId": 3,
      "title": "title 3",
      "content": "content 3"
    }
  ]
}

وملف يدعى ads.json يمثل الاعلانات

{
  "ads": [
    {
      "id": 1,
      "title": "title 1",
      "sponser": "sponser 1"
    },
    {
      "id": 2,
      "title": "title 2",
      "sponser": "sponser 2"
    },
    {
      "id": 3,
      "title": "title 3",
      "sponser": "sponser 3"
    }
  ]
}

ولدينا دالة تدعى getFromDatabase تأخذ model تمثل ما نريد احضارة من قاعدة البيانات

const getFromDatabase = async (model) => {
  console.log(`fetching ${model}`);

  const res = await fetch(`./${model}.json`);
  const data = await res.json();

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(data);
      console.log(`complete fetching ${model}`);
    }, Math.random() * 1000);
  });
};

الدالة ترجع لنا promise كما ترى
وستلاحظ وجود setTimeout مدته قيمة عشوائية Math.random() * 1000 لكي نحاكي المدة العشوائية التي يستغرقها كل Request
لانه قد يختلف في كل مرة بسبب عوامل كثيرة ومختلفة

ولدينا دالة بسيطة تدعى render تستقبل بيانات
وكأنها تقوم بعمل render للبيانات بطباعتها

function render(data) {
  console.log('render:', data);
  console.log('\n');
}

جلب البيانات بشكل Sequential

async function getData() {
  try {
    const users = await getFromDatabase('users');
    render(users);

    const posts = await getFromDatabase('posts');
    render(posts);

    const ads = await getFromDatabase('ads');
    render(ads);
  } catch (err) {
    console.log(err);
  }
}

هنا لدينا ثلاثة Promise وكل واحدة ستنتظر البيانات، وهم getFromDatabase('users'), getFromDatabase('posts'), getFromDatabase('ads')

ثم بعد انتهاء كل واحدة يتم عمل لها render للصفحة الرئيسية على سبيل المثال

السؤال هنا كيف سيتم تنفيذ هذا الكود ؟

سيتم تنفيذه بشكل sequential أي سيتم تنفيذ أول promise ثم بعد انتهائها سيتم تنفيذ الثانية ثم بعد انتهائها سيتم تنفيذ الثالثة

fetching users
complete fetching users
render: {users: Array(3)}

fetching posts
complete fetching posts
render: {posts: Array(3)}

fetching ads
complete fetching ads
render: {ads: Array(3)}

هل استنتجت المشكلة ؟ كل promise ستضطر لانتظار الـ promise السابقة لكي تبدأ هى بالتنفيذ لنعد لنفس السؤال الذي سألناه سابقًا هل هذه العمليات مرتبطة ببعض ؟ لو كانت الإجابة نعم فنستطيع استعمل الـ sequential دون أي اعتراض
لو كانت الإجابة لا، فإذا يجب ألا نستعمل طريقة الـ sequential
لما نجعل كل promise تنتظر سابقتها لكي تبدأ برغم من انهم عمليات مستقلة تمامًا ليست مرتبطة ببعضهما البعض
لذا يجب أن ننفذ كل promise بشكل مستقل دون أن تأثر واحدة على الأخرى

استخدام Promise.then

function getData() {
  getFromDatabase('users').then(function (users) {
    render(users);
  });

  getFromDatabase('posts').then(function (posts) {
    render(posts);
  });

  getFromDatabase('ads').then(function (ads) {
    render(ads);
  });
}

سيتم تنفيذ الكود بشكل parallelism أي سينفذ كل promise بشكل مستقل عن الأخرى
وعندما ينتهي تنفيذ أي promise سيتم تنفيذ دالة الـ render الخاصة بها

fetching users
fetching posts
fetching ads

complete fetching ads
render: {ads: Array(3)}

complete fetching users
render: {users: Array(3)}

complete fetching posts
render: {posts: Array(3)}

ماذا لاحظت ؟
لقد تم عمل fetching لجميع الـ promise في آن واحد
لكن، ستلاحظ أنهم انتهوا في اوقات مختلفة
لأن الـ then كما نعرف تنفذ الكود بعد ما ينتهي الـ promise
ويتم تطبيق هذا الأمر بشكل asynchronous بمعنى أن كل promise لا تتأثر بالأخرى
وكل واحدة تنفذ بشكل مستقل لهذا هذه الطريقة تعتبر parallelism

هذا حل جيد ومفيد في حلات كثيرة، لكن المشكلة هنا تكمن ان كل promise ينفذ بشكل مستقل تمامًا

فماذا لو أردنا تنفيذ كل الـ promise بشكل مستقل كما نحن
لكن بدلًا من أن كل واحدة تنفذ بعد أن تنتهي نريد الانتظار حتى اكتمالها جميعًا قبل المتابعة إلى الخطوة التالية في الكود

وهنا تأتي ميزة ووظيفة الـ Promise.all

استخدام Promise.all

وهذا الحل سيكون حل concurrency أي ننفذ كل الـ promise في آن واحد لكن بشكل متزامن

حسنًا، Promise.all() وهي دالة كما يوحي الاسم أنها تتعامل مع أكثر من Promise في آن واحد
المميز فيها انها تتعامل معهم بشكل concurrency على عكس طريقة الـ Promise.then() الذي كان يتعامل مع كل promise بشكل مستقل أي parallelism

عليك ان تعرف أن Promise.all() تستقبل أراي من الـ promise بهذا الشكل Promise.all([promise1, promise2, promise3])
ويرجع لنا أراي بنواتج كل الـ promise بالترتيب بعد أن ينتهوا

async function getData() {
  try {
    const results = await Promise.all([
      getFromDatabase('users'),
      getFromDatabase('posts'),
      getFromDatabase('ads'),
    ]);
    results.forEach(function (result) {
      render(result);
    });
  } catch (err) {
    console.log(err);
  }
}

لاحظ كيف ارسلنا الـ promise الثلاثة لها
وقمنا بعمل await لانها ترجع promise بالناتج
قيمة results يحتوي على أراي بنتائج كل promise بالترتيب
بمعنى أن results[0] ناتج لـ getFromDatabase('users')
و results[1] ناتج لـ getFromDatabase('posts')
و results[2] ناتج لـ getFromDatabase('ads')

fetching users
fetching posts
fetching ads

complete fetching users
complete fetching ads
complete fetching posts

render: {users: Array(3)}
render: {posts: Array(3)}
render: {ads: Array(3)}

ماذا لاحظت ؟
ستلاحظ أنه تم عمل fetching لكل promise في آن واحد
ثم جميعهم اكتملوا في آن واحد
ثم عملنا render لجميعهم في آن الواحد

هذا هو فكرة الـ concurrency في الـ Promise.all

بما أن Promise.all ترجع لنا promise فإذا يمكننا استخدام then معها

function getData() {
  Promise.all([
    getFromDatabase('users'),
    getFromDatabase('posts'),
    getFromDatabase('ads'),
  ]).then(function (results) {
    results.forEach(function (result) {
      render(result);
    });
  });
}

يمكنك استخدام أي طريقة تفضلها

استخدام Promise.allSettled إذا حدث أي مشكلة في أحد الـ Promise

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

function getData() {
  try {
    const results = await Promise.all([
      getFromDatabase('users'),
      getFromDatabase('post'), // Typo Error: will throw an exception
      getFromDatabase('ads'),
    ]);

    results.forEach(function (result) {
      render(result);
    });
  } catch (err) {
    console.log(err);
  }
}

ماذا سيحدث ؟

fetching users
fetching post
fetching ads

EXCEPTION ERROR: post is not found
complete fetching users
complete fetching ads

لاحظ انه اكمل جميع الـ promise لكنه عندما وجد خطا ما
قام بعمل Exception وهذا ادى الى توقف الكود وتشغيل الـ catch

بمعنى انه بسبب مشكلة واحدة في احد الـ promise
جعل الكود كله يتوقف ولا يحصل أي render لأي شيء

فلماذا نجعل كودنا يتوقف بأكمله بسبب promise ما حصل له خطأ معين ؟

هنا تظهر فكرة الـ Promise.allSettled
وهي تقوم بعمل نفس وظيفة Promise.all لكن عندما تظهر أي مشكلة في أي promise
يستمر في تنفيذ الباقي دون توقف ولا يقوم بعمل Exception

function getData() {
  try {
    const results = await Promise.allSettled([ // use Promise.allSettled
      getFromDatabase('users'),
      getFromDatabase('post'), // Typo Error: will continue without throw an exception
      getFromDatabase('ads'),
    ]);

    results.forEach(function (result) {
      if (result.status === 'fulfilled') render(result.value);
    });
  } catch (err) {
    console.log(err);
  }
}

ماذا سيحدث الآن ؟

fetching users
fetching post
fetching ads

complete fetching users
complete fetching ads

render: {users: Array(3)}
render: {ads: Array(3)}

لقد قام بعمل fetching لكل promise كالعادة
لاحظ أن الـ promise الخاص بـ users و ads اكتملوا دون مشاكل
وبرغم من أن posts حصل فيها مشكلة لكنه لم يقم بعمل Exception بل أكمل تنفيذ الكود

أما في جزء الـ render فستلاحظ وجود شرط ما وهو result.status === 'fulfilled'
السبب هو أن Promise.allSettled يقوم بارجاع لنا النتائج بشكل مختلف عن Promise.all

حيث أن Promise.all كان يرجع لنا أراي من النتائج بشكل مباشر

render: {users: Array(3)}
render: {posts: Array(3)}
render: {ads: Array(3)}

أما Promise.allSettled سيرجعها لنا بهذا الشكل

render: {status: 'fulfilled', value: {users: Array(3)}}
render: {status: 'rejected', reason: 'ERROR: post is not found'}
render: {status: 'fulfilled', value: {ads: Array(3)}}

لاحظ أنه اصبح لك يعطيه بيانات أخرى كـ status لتعرف هل هذا الـ promise نفذ دون مشاكل أم لا
فاذا اكتمل تنفيذ الـ promise دون مشاكل، فستكون قيمة الـ status تساوي fulfilled وسيعطيك النتائج في value
واذا حصل أي مشكلة في أي promise فستكون قيمة الـ status تساوي rejected وسيعطيك سبب المشكلة في reason

لهذا قمنا بعمل هذا الشرط البسيط لنحدد أي promise اكتمل بنجاح لنقوم بعمل render لبياناته

if (result.status === 'fulfilled') render(result.value);

لاحظ أن Promise.allSettled لا يقوم بعمل أي Exception

استخدام Promise.race

حسنًا لدينا دالة اخرى غريبة بعض الشيء وهي Promise.race
وهي مثل Promise.all تستقبل أراي من الـ promise
لكن هنا هو سيرجع لنا نتيجة واحدة فقط
وهي أول promise تكتمل سيرجع لنا نتيجتها ويهمل الباقي

كأن الـ promise كلها في سباق وأول واحدة تكتمل بنجاح سنحصل على قيمتها

function getData() {
  try {
    const result = await Promise.race([ // use Promise.race
      getFromDatabase('users'),
      getFromDatabase('posts'),
      getFromDatabase('ads'),
    ]);

    // the first promise finished, we will render it
    render(result);
  } catch (err) {
    console.log(err);
  }
}

ماذا سيحدث الآن ؟

fetching users
fetching posts
fetching ads

complete fetching ads

render: {ads: Array(3)}

complete fetching posts
complete fetching users

لاحظ أنه قام بعمل fetching لكل promise كالعادة
ومع أول promise اكتملت وهي ads قام بتنفيذ باقي الكود وعمل render له واهمل الباقي
ولاحظ أن باقي الـ promise اكتملوا في الخلفية لكن لم يعير لها أي اهتمام
لانه ينفذ ويرجع لنا أول promise انتهت واكتملت

for await of

حسنًا وصلنا لنهاية مقالتنا القصيرة مع آخر طريقة هي for-await-of

async function getData() {
  try {
    const promises = [
      getFromDatabase('users'),
      getFromDatabase('posts'),
      getFromDatabase('ads'),
    ];

    for await (const result of promises) {
      render(result);
    }
  } catch (err) {
    console.log(err);
  }
}

وهي طريقة بسيطة جدًا كما ترى
إذا كان لديك أراي من الـ promise تستطيع فقط أن تقوم بعمل loop على كل عناصرها
من خلال الـ for-await-of بأن تقوم بعمل await لجميع الـ promise
بشكل parallelism ثم بعد ما جميهم يكتملوا
تقوم الـ for-await-of بتنفيذهم بالترتيب

fetching users
fetching posts
fetching ads

complete fetching posts
complete fetching users

render: {users: Array(3)}
render: {posts: Array(3)}

complete fetching ads

render: {ads: Array(3)}

ركز هنا جيدًا، ماذا تلاحظ ؟

حسنًا، قام بعمل fetching لكل promise كالعادة
لاحظ أن الـ promise الخاص بالـ posts أكتمل أولًا، لكن لم يقم بعمل له render
وحين انتهى تنفيذ الـ promise الخاص بالـ users قام بعمل له render فورًا
ثم قام بعمل render للـ posts بالرغم بأن الـ posts سبقت الـ users
ما السبب ؟ السبب أن for-await-of ينفذ بحسب الترتيب الموجود في الأراي الذي يقوم بتنفيذها
لا يعير أي اهتمام بمن سبق من أو من انتهى قبل من
ما يهتم به انه يبدأ بتنفيذهم بنفذ ترتيبهم

لاحظ أيضًا انه عندما لو كان أول promise تكتمل هي أول عنصر في الأراي سيقوم بتنفيذها فورًا
لانه يعطي الأولوية لترتيبهم في الأراي

وأيضًا نستنتج أنه اذا اكتمل كل الـ promise بهذا الشكل

complete fetching posts
complete fetching ads
complete fetching users

فسيقوم بعمل render لهم بنفس ترتيبهم في الأراي

render: {users: Array(3)}
render: {posts: Array(3)}
render: {ads: Array(3)}