درود دوستان 👋 توی این پست میخوایم با متدهای استاتیک Promise ها توی جاوااسکریپت آشنا بشیم. این متدها شامل:
این متدها که از کلاس گلوبال Promise به صورت استاتیک قابل دسترس هستن (یعنی مثلاً Promise.resolve() یا Promise.race()) کاربردهای زیادی دارن و بهمون کمک میکنن یک برنامهٔ سریعتر و پویاتر داشته باشیم. توی این پست این متدها رو بررسی میکنیم و با مثالهایی از کاربرد اونها توی دنیا واقعی آشنا میشیم.
متد استاتیک چیه؟ 🤔
متد استاتیک به متدهایی گفته میشه که بدون نیاز به ساختن نمونه از یک کلاس قابل دسترس هستن. برای مثال همونطور که میدونیم متدهایی مثل then و catch فقط زمانی در دسترس هستن که یک آبجکت از Promise داشته باشیم:
const obj = new Promise(...); obj.then(...); obj.catch(...);
به این متدها گفته میشه Instance Methods و فقط توی نمونهها (آبجکتهای ساخته شده از کلاس) قابل دسترس هستن. اما از خود کلاس Promise متدهایی قابل دسترس هستن که بدون نیاز به ساختن آبجکت از میتونیم از اونها استفاده کنیم. مثل:
Promise.all(...) Promise.any(...);
به این متدها گفته میشه متدهای استاتیک. متدهای استاتیک کاربردهای منحصر به فردی دارن که اونها رو از متدهای Instance متمایز میکنه که امیدوارم بتونیم اونها رو توی پست جداگونه بررسی کنیم. توی جاوااسکریپت نمونههای زیادی از متدهای استاتیک وجود داره مثل متد floor از کلاس Math. یا isArray از Array.
نکته: توی توضیحات بالا منظور از کلاس همون Constructor Function هست که توی جاوااسکریپت کاربردی شبیه کلاسها توی برنامهنویسی شیء گرایی داره.
قبل از شروع، اگه با پرامیسها آشنایی ندارین:
خب بریم که این متدها رو بررسی کنیم.
Promise.resolve()
این متد بلافاصله یک پرامیس fulfilled شده با مقدار مد نظرمون رو به خروجی میده:
const p = Promise.resolve(10); p.then(value => alert(value)); // 10
اگه بخوایم کد بالا رو بدون استفاده از متد resolve() داشته باشیم باید چنین چیزی بنویسیم:
const p = new Promise(resolve => resolve(10)); p.then(value => alert(value)); // 10
استفاده از این متد برای شرایطی خوبه که میخوایم اطلاعات رو تحت هر شرایطی به شکل پرامیس رد و بدل کنیم. مثلاً تابعی که توی بعضی شرایط یک پرامیس رو برمیگردونه و توی بعضی از شرایط دیگه یک مقدار آرایهای، بهتره که مقدار آرایهای رو هم به صورت پرامیس برگردونیم تا برنامهٔ ما قابل اطمینان و به قول معروف Consistent باشه و افرادی که از این تابع استفاده میکنن مطمئن باشن که خروجی این تابع همیشه یک پرامیس هست:
function users() { const usersInMemory = Cache.get('users'); if (usersInMemory) { return Promise.resolve(usersInMemory); } return Api.get('users'); }
Promise.reject()
این متد هم مشابه متد قبل هست با این تفاوت که بلافاصله یک پرامیس rejected شده رو با مقدار دلخواه به خروجی میده:
const p = Promise.reject('Failed'); p.catch(err => alert(err));
فرض کنیم تابعی داریم که یک پرامیس رو به خروج میده. اما توی شرایطی این تابع خطاهایی رو هم میتونه برگردونه:
function users(limit = 10) { if (typeof limit !== 'number') { return new Error('limit must be a number'); } return Api.get('users'); }
توی کد بالا توی خط ۳ داریم مقداری رو ریترن میکنیم که پرامیس نیست. در حالیکه توی خط ۶ مقداری که از اون متد میاد یک پرامیس هست. بنابراین این تابع باعث مشکلاتی توی برنامه میشه و برنامهنویس همیشه باید بررسی کنه که خروجی این تابع چی هست. پس یک راه بهتر استفاده از Promise.reject() توی چنین سناریوهایی هست تا برنامهٔ ما قابل اطمینان باشه:
function users(limit = 10) { if (typeof limit !== 'number') { return <<Promise.reject>>(new Error('limit must be a number')); } return Api.get('users'); }
کد بالا همیشه یک پرامیس رو ریترن میکنه و بنابراین برنامهنویس همیشه با اون مثل یک پرامیس رفتار خواهد کرد.
Promise.all()
این متد آرایهای از چند پرامیس دیگه رو میگیره و خروجی اون یک پرامیس هست. و زمانی این پرامیس fulfilled میشه که همهٔ پرامیسهایی که به اون پاس دادیم کارشون با موفقیت به پایان برسه و به اصلاح fulfilled بشن:
const result = Promise.all([ Promise.resolve(), Promise.resolve(), Promise.resolve(), Promise.resolve(), ]); result.then(() => alert('Done!'));
چیزی که به then پاس داده میشه، یک آرایه از همهٔ مقادیری هست که پرامیسهای داخلی برمیگردونن:
const result = Promise.all([ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4), ]); result.then((values) => { alert(Array.isArray(values)); // true alert(values[3]); // 4 });
توی Promise.all اگه فقط یکی از پرامیسهای پاس داده شده با خطا مواجه بشه، خروجی پرامیس نهایی rejected خواهد بود:
const result = Promise.all([ Promise.resolve(), Promise.resolve(), <<Promise.reject()>>, Promise.resolve(), ]); result.then(() => alert('Done!')); // Will not run result.catch(() => alert('Failed!'));
توی کد بالا پرامیس سوم آرایه reject شد. بنابراین نتیجه کلی اون پرامیس rejected شد. صرف نظر از نتیجه بقیه پرامیسها.
چه زمانی از Promise.all() استفاده کنیم؟ 🤔
اگه چندین پرامیس داریم و میخوایم به صورت همزمان اونها رو اجرا و مدیریت کنیم و همچنین fulfilled شدن همهٔ اونها برامون اهمیت داره، بهتره که از Promise.all() استفاده کنیم. از مهمترین مزیت استفاده از این متد اینه که اجازه میده همهٔ عملیات به صورت یکجا و موازی شروع بشن و بنابراین باعث افزایش سرعت و عملکرد برنامه میشه:
async function render() { // Bad ❌ runs in sequence, poor performance const users = await Api.get('users'); const posts = await Api.get('posts'); const shows = await Api.get('shows'); // ... // Good ✅: runs in parallel, better performance const data = Promise.all([ Api.get('users'), Api.get('posts'), Api.get('shows'), ]); }
Promise.allSetteled()
این متد که تقریباً مشابه all() هست، آرایهای از پرامیسها رو به عنوان ورودی میگیره و زمانی fulfilled میشه که همهٔ پرامیسها Settle بشن. منظور از Settle شدن یعنی کار پرامیس تموم بشه (فارغ از اینکه نتیجه پرامیس چی باشه. یعنی fulfilled یا rejected اهمیتی نداره.)
نحوهٔ استفاده از allSettled به این صورت هست:
const promise1 = Promise.resolve(10); const promise2 = Promise.reject('Failed'); const promise3 = Promise.resolve('Success'); Promise.allSettled([ promise1, promise2, promise3, ]) .then((results) => console.log(results)); // 0: Object { status: "fulfilled", value: 10 } // 1: Object { status: "rejected", reason: "Failed" } // 2: Object { status: "fulfilled", value: "Success" }
با توجه به اینکه پرامیسی که از متد allSettled ریترن میشه همیشه fulfilled میشه، بنابراین متد catch. برای این متد کار نخواهد کرد.
چه زمانی از Promise.allSettled() استفاده کنیم؟ 🤔
این متد برای زمانی خوبه که چندین عملیات Async داریم که کاملاً مستقل از همدیگه هستن و خطا داشتن یک کدوم نباید تأثیر بذاره روی بقیه عملیات. اما میخوایم که بعد از تموم شدن همهٔ عملیات کاری انجام بدیم. مثلاً لود کردن اطلاعات بخشهای مختلف و مستقل یک برنامه:
setLoading(true); const result = Promise.allSettled([ getUsers(), getPosts(), getFooterLinks(), ]); result.then(() => setLoading(false));
Promise.any()
این متد آرایهای از پرامیسها رو میگیره و خروجی اون یک پرامیس دیگه هست. این پرامیسِ خروجی به محض اینکه یکی از پرامیسهای پاسداده شده fulfilled بشه، rosolve میشه با مقدار اون پرامیسی که fulfilled شده:
const result = Promise.any([ wait(2000), wait(1000), wait(3000), wait(5000), ]); result.then(alert); // 1000 function wait(ms) { return new Promise(resolve => setTimeout(() => resolve(ms), ms)); }
همونطور که میبینیم چیزی که به then پاس داده میشه اینبار دیگه آرایه نیست. بلکه تک مقدار هست و اون پرامیسی هست که fulfilled شده.
پرامیسی که توسط متد Promise.any ریترن میشه، زمانی rejected میشه که همهٔ پرامیسهای پاسداده شده reject بشن. برای مدیریت کردن این پرامیس که رجکت شده، باید از متد catch استفاده کنیم:
const result = Promise.any([ Promise.reject(1), Promise.reject(2), Promise.reject(3), Promise.reject(4), ]).catch(error => { if (error instanceof AggregateError) { alert('All promises failed.' + error.errors); // 1,2,3,4 } else { alert('Unexpected error:' + error); } });
همونطور که میبینیم با رجکت شدن Promise.any() آبجکت error که به catch پاس داده شده، یک آبجکت از AggregateError هست.
چه زمانی از Promise.any() استفاده کنیم؟ 🤔
چیزی که برای این متد اهمیت داره اولین پرامیسی هست که fulfilled میشه. بنابراین با این اتفاق، بقیه پرامیسها صرف نظر از اینکه fulfilled بشن یا rejected، نادیده گرفته میشن. برای مثال یک ریسورس (عکس، اسکریپت و ...) داریم که روی سرورهای مختلف قرار گرفته شده و حق انتخاب داریم که از کدوم سرور این ریسورس رو لود کنیم. اما سرعت لود برای ما اهمیت داره. همچنین زمانهایی وجود داره که میخوایم عملکرد و سرعت چندین عملیات Async رو محاسبه کنیم و به قول معروف عملیات Benchmarking انجام بدیم. بنابراین استفاده از Promise.any() توی چنین شرایطی راه حل مناسبی به حساب میاد.
Promise.race()
این متد که بهنحوی هم به متد any و هم به allSettled شباهت داره، آرایهای از چند پرامیس رو قبول میکنه و خروجی اون یک پرامیس هست. این پرامیسِ خروجی متد race به محض اینکه یکی از پرامیسهای پاسداده شده کارش به پایان برسه، با وضعیت همون پرامیس Settle میشه. یعنی وضعیت fulfilled/rejected شدن پرامیسها براش مهم نیست. برای مثال اگه اولین پرامیسی که کارش به پایان میرسه fulfilled بشه، بلافاصله پرامیس خروجی متد race هم fulfilled میشه با همون مقدار. و همینطور برای rejected شدن:
const result = Promise.race([ wait_reject(500), wait_reject(300), wait_reject(1300), wait_reject(900), ]); result.then( value => alert(value), // Not called error => alert(error), // 300 ); function wait_reject(ms) { return new Promise((_, reject) => setTimeout(() => reject(ms), ms)); }
چون اولین پرامیسی کارش به پایان رسید (دومین پرامیس اون آرایه) بعد از ۳۰۰ میلیثانیه rejected شد، نتیجهٔ پرامیس result هم rejected شد، و بنابراین کد خط ۱۰ برامون اجرا شد. که البته اون قسمت رو میتونستیم بهصورت جداگونه با متد catch هم مدیریت کنیم:
result.catch(error => alert(error);
به همین صورت، اگه پرامیسی توی اون آرایه fulfilled میشد، کد خط ۹ برامون اجرا میشد به این معنی که پرامیس result هم fulfilled شده.
چه زمانی از Promise.race() استفاده کنیم؟ 🤔
استفاده از این متد برای شرایطی خوبه که نتیجهٔ عملیات برامون مهم نیست و چیزی که اهمیت داره اینه که کدوم عملیات زودتر به پایان میرسه. یکی از جاهایی که میتونیم از این متد استفاده کنیم اینه که برای مثال قصد داریم چندین عملیات Async رو اجرا کنیم با این شرط که نمیخوایم بیشتر از یک مدت زمان مشخص برای دریافت نتیجه صبر کنیم:
function timeout(ms) { return new Promise((_, reject) => setTimeout(reject, ms)); } const tasks = Promise.race([ task1(), task2(), task3(), task4(), // ... timeout(500), ]); tasks.then(...); tasks.catch(() => alert('Request timeout'));
توی کد بالا گفتیم که اگه اجرای عملیات بیشتر از ۵۰۰ میلیثانیه طول بکشه (خط ۱۱)، دیگه نتیجه عملیات برامون مهم نیست.
خب دوستان امیدوارم از این پست استفاده کرده باشین. روزتون خوش 🌹👋
