Promise.all در جاوا اسکریپت — از صفر تا صد

Promise-ها در جاوا اسکریپت یکی از API-های قدرتمند هستند که به اجرای عملیات ناهمگام کمک میکنند. Promise.all عملیات ناهمگام را به سطح بالاتری ارتقا داده است و به گروهبندی promise-ها کمک کرده است. به بیان دیگر با استفاده از Promise.all در جاوا اسکریپت میتوان گروهی از عملیات «همزمان» (concurrent) را اجرا کرد.
پیشنیاز مطالعه این مطلب آن است که با مفهوم Promise در جاوا اسکریپت آشنا باشید. اگر چنین نیست پیشنهاد میکنیم، مقاله «Promise در جاوا اسکریپت و کاربردهای آن — به زبان ساده» را مطالعه کنید.
Promise.all چیست؟
Promise.all در واقع یک promise است که ارائه از Promise-ها را به عنوان ورودی (عنصر تکرارپذیر = iterable) میگیرد. سپس وقتی که همه promise-ها پاسخ داده شوند یا هر یک از آنها رد شوند، Promise.all نیز به پایان میرسد.
برای نمونه فرض کنید 10 Promise دارید که عملیات ناهمگامی برای یک فراخوانی شبکه یا اتصال پایگاه داده اجرا میکنند. لازم است که بدانید همه Promise-ها چه زمانی پاسخ داده میشوند و یا لازم است صبر کنید تا همه آنها به پایان برسند. بدین ترتیب همه آنها را به Promise.all میفرستید و سپس Promise.all خودش به عنوان یک Promise زمانی که هر 10 Promise پاسخ داده شوند و یا هر یک از آنها رد شوند، به پایان میرسد.
به کد زیر توجه کنید:
Promise.all([Promise1, Promise2, Promise3]) .then(result) => { console.log(result) }) .catch(error => console.log(`Error in promises ${error}`))
همان طور که میبینید، ما یک ارائه را به Promise.all ارسال کردهایم و زمانی که هر سه Promise پاسخ داده شوند، Promise.all نیز پایان مییابد و خروجی در کنسول ارائه میشود.
به مثال زیر نیز توجه کنید:
// A simple promise that resolves after a given time const timeOut = (t) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`Completed in ${t}`) }, t) }) } // Resolving a normal promise. timeOut(1000) .then(result => console.log(result)) // Completed in 1000 // Promise.all Promise.all([timeOut(1000), timeOut(2000)]) .then(result => console.log(result)) // ["Completed in 1000", "Completed in 2000"]
در مثال فوق، Promise.all در طی 2000 میلیثانیه پس از این که خروجی به صورت یک ارائه در کنسول ارائه شود پایان مییابد.
ترتیب Promise-ها
یک نکته جالب در مورد Promise.all این است که ترتیب Promise-ها در آن حفظ میشود. نخستین Promise در ارائه به عنوان نخستین عنصر ارائه خروجی ارائه میشود، Promise دوم عنصر دوم ارائه است و همین طور تا آخر.
به مثال زیر نیز توجه کنید:
// A simple promise that resolves after a given time const timeOut = (t) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`Completed in ${t}`) }, t) }) } const durations = [1000, 2000, 3000] const promises = [] durations.map((duration) => { // In the below line, two things happen. // 1. We are calling the async function (timeout()). So at this point the async function has started and enters the 'pending' state. // 2. We are pushing the pending promise to an array. promises.push(timeOut(duration)) }) console.log(promises) // [ Promise { "pending" }, Promise { "pending" }, Promise { "pending" } ] // We are passing an array of pending promises to Promise.all // Promise.all will wait till all the promises get resolves and then the same gets resolved. Promise.all(promises) .then(response => console.log(response)) // ["Completed in 1000", "Completed in 2000", "Completed in 3000"]
همان طور که میبینید اگر یکی از Promise-ها رد شود، همه موارد باقیمانده Promise-ها نیز شکست میخورند. در این صورت Promise.all نیز رد خواهد شد.
در برخی موارد ما به چنین موقعیتی نیاز نداریم. در واقع در این موارد لازم است که همه Promise-ها صرف نظر از این که یکی از آنها رد شود یا نه اجرا شوند و احتمالاً مورد رد شده را نیز در ادامه میتوان مدیریت کرد. مدیریت این وضعیت به صورت زیر ممکن است:
const durations = [1000, 2000, 3000] promises = durations.map((duration) => { return timeOut(duration).catch(e => e) // Handling the error for each promise. }) Promise.all(promises) .then(response => console.log(response)) // ["Completed in 1000", "Rejected in 2000", "Completed in 3000"] .catch(error => console.log(`Error in executing ${error}`))
کاربردهای Promise.all
فرض کنید لازم است تعداد زیادی عملیات ناهمگام مانند ارسال انبوه ایمیلهای بازاریابی به هزاران کاربر را انجام دهید.
شبه کد ساده آن چنین میتواند باشد:
for (let i=0;i<50000; i += 1) { sendMailForUser(user[i]) // Async operation to send a email }
مثال فوق سرراست است. اما چندان کارآمد نیست. در نقطهای از زمان، پشته بسیار سنگین میشود و جاوا اسکریپت با انبوهی از درخواستهای باز اتصال HTTP مواجه خواهد بود که میتوانند سرور را کار بیندازند.
یک رویکرد ساده و کارآمد میتواند این باشد که این کار در دستهبندیهای مختلف انجام یابد. 500 کاربرِ نخست انتخاب و ایمیلها ارسال میشوند و صبر میکنیم تا همه اتصالهای HTTP بسته شوند. سپس دسته بعدی را پردازش میکنیم و همین طور تا آخر. مثال زیر را در نظر بگیرید:
// Async function to send mail to a list of users. const sendMailForUsers = async (users) => { const usersLength = users.length for (let i = 0; i < usersLength; i += 100) { const requests = users.slice(i, i + 100).map((user) => { // The batch size is 100. We are processing in a set of 100 users. return triggerMailForUser(user) // Async function to send the mail. .catch(e => console.log(`Error in sending email for ${user} - ${e}`)) // Catch the error if something goes wrong. So that it won't block the loop. }) // requests will have 100 or less pending promises. // Promise.all will wait till all the promises got resolves and then take the next 100. await Promise.all(requests) .catch(e => console.log(`Error in sending email for the batch ${i} - ${e}`)) // Catch the error. } } sendMailForUsers(userLists)
سناریوی دیگری را در نظر بگیرید. فرض کنید لازم است یک API بسازیم که اطلاعاتی را از API-های شخص ثالث میگیرد و همه پاسخها را از آن API-ها تجمیع میکند.
در این حالت Promise.all بهترین روش برای انجام این کار است. چگونگی آن را در کد زیر ملاحظه میکنید:
// Function to fetch Github info of a user. const fetchGithubInfo = async (url) => { console.log(`Fetching ${url}`) const githubInfo = await axios(url) // API call to get user info from Github. return { name: githubInfo.data.name, bio: githubInfo.data.bio, repos: githubInfo.data.public_repos } } // Iterates all users and returns their Github info. const fetchUserInfo = async (names) => { const requests = names.map((name) => { const url = `https://api.github.com/users/${name}` return fetchGithubInfo(url) // Async function that fetches the user info. .then((a) => { return a // Returns the user info. }) }) return Promise.all(requests) // Waiting for all the requests to get resolved. } fetchUserInfo(['sindresorhus', 'yyx990803', 'gaearon']) .then(a => console.log(JSON.stringify(a))) /* Output: [{ "name": "Sindre Sorhus", "bio": "Full-Time Open-Sourcerer ·· Maker ·· Into Swift and Node.js ", "repos": 996 }, { "name": "Evan You", "bio": "Creator of @vuejs, previously @meteor & @google", "repos": 151 }, { "name": "Dan Abramov", "bio": "Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.", "repos": 232 }] */
سخن پایانی
در پایان باید اشاره کنیم که Promise.all بهترین روش برای تجمیع یک گروه از Promise-ها در یک Promise منفرد است. این یکی از بهترین روشها برای رسیدن به «همزمانی» (Concurrency) در جاوا اسکریپت است. امیدواریم از مطالعه این نوشته بهره برده باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکریپت
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- Promise در جاوا اسکریپت و کاربردهای آن — به زبان ساده
- آموزش جاوا اسکریپت مقدماتی: ساخت بازی حدس اعداد — به زبان ساده
==