التعامل مع أكثر من Promise في آن واحد
السلام عليكم ورحمة الله وبركاته
الفهرس
المقدمة
شرحنا في مقالة سابقة عن الـ 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)}