وعده: نحوه استفاده و چند نکته!

صحبت نکردن در مورد Promise در جاوا اسکریپت یک حذف آشکار است. در واقع، مقالات زیادی در مورد Promise نوشته شده است که می توانید آنها را از طریق گوگل پیدا کنید یا گهگاه در انجمن های مرتبط با برنامه نویسی با آنها برخورد کنید. اما از آنجایی که Promise یک مفهوم مهم است و هر کسی روش متفاوتی برای توضیح آن دارد، من همچنان تصمیم گرفتم این مقاله را بنویسم.
وقتی برای اولین بار جاوا اسکریپت را یاد گرفتم، Promise گیج کننده ترین چیز بود. فکر میکردم آن را میفهمم و میدانم چگونه از آن استفاده کنم، اما در واقعیت، هنوز بسیاری از دست و پا زدنهای طولانی و دردناک وجود داشت که درسهای ارزشمندی به من آموخت. من مقالات زیادی در مورد Promise به دو زبان انگلیسی و ویتنامی خواندم و به تدریج همه چیز معنی پیدا کرد و به من کمک کرد تا آن را به درستی درک کنم و از آن استفاده کنم.
این مقاله به مفاهیم Promise نمی پردازد، بلکه به برخی نکات و سوء تفاهمات احتمالی می پردازد. هدف آن ارائه یک درک کلی به خوانندگان و کمک به آنها برای جلوگیری از برخی اشتباهات هنگام نوشتن کد است.
وعده چیست؟
Promise یک شی است که نشان دهنده نتیجه آینده است. به عبارت دیگر، یک Promise نتیجه یک تابع ناهمزمان را نشان می دهد.
یک Promise دارای سه حالت مربوط به سه نتیجه ممکن یک تابع ناهمزمان است:
-
pending
حالت اولیه است، منتظر نتیجه است. -
fulfilled
حالت موفق با مقدار نتیجه است. -
rejected
حالت شکست، با مقدار خطای اختیاری است.
به عنوان مثال، بیایید یک Promise ایجاد کنیم که یک عدد را می گیرد x
و برمی گرداند fulfilled
اگر x
بر 2 بخش پذیر است و rejected
در غیر این صورت بیان کنید
function isEven(x) {
return new Promise((resolve, reject) => {
if (x % 2 === 0) {
resolve(true);
} else {
reject(new Error('x is not even'));
}
});
}
در اصل، یک وعده نشان دهنده یک نتیجه آینده است. در مثال بالا، ایجاد یک Promise ضروری نیست زیرا تمام اقدامات داخل آن انجام می شود isEven
تابع همزمان هستند بنابراین، چه زمانی باید از Promise استفاده کنیم و چه چیزی یک تابع را ناهمزمان می کند؟
چه چیزی یک تابع را ناهمزمان می کند؟ من در بسیاری از مقالات به آن اشاره کرده ام. برخی از وظایف I/O ارائه شده توسط توابع ناهمزمان نمی توانند فوراً نتیجه را برگردانند. آنها به عوامل خارجی مانند سرعت سخت افزار، سرعت شبکه و غیره بستگی دارند. انتظار برای این اقدامات می تواند زمان زیادی را هدر دهد یا باعث تنگناهای جدی شود.
به عنوان مثال، هنگام درخواست GET به آدرس https://example.com
، پردازش دیگر به سرعت CPU بستگی ندارد. به سرعت شبکه شما نیز بستگی دارد. هر چه شبکه سریعتر باشد، نتیجه را سریعتر دریافت خواهید کرد. در جاوا اسکریپت، ما آن را داریم fetch
تابع ارسال درخواست است و ناهمزمان است و یک Promise را برمی گرداند.
fetch('https://example.com');
برای رسیدگی به نتیجه یک Promise، از آن استفاده می کنیم then
و catch
به ترتیب برای موارد موفقیت و شکست.
fetch('https://example.com')
.then(res => console.log(res))
.catch(err => console.log(err));
اگر پس از مدتی پردازش، fetch
است fulfilled
، تابع داخل then
فعال خواهد شد. در غیر این صورت، اگر fetch
است rejected
، تابع داخل catch
بلافاصله اعدام خواهد شد.
گاهی اوقات ممکن است با سوالاتی مانند پیش بینی نتیجه مثال زیر مواجه شوید:
console.log('1');
fetch('https://example.com')
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('3'));
console.log('2');
این سوال در واقع برای آزمایش درک شما از رفتار ناهمزمان در جاوا اسکریپت است. نتیجه به جای 1، 3، 2، 1، 2، 3 خواهد بود. این به دلیل ماهیت پردازش ناهمزمان است. از آنجایی که نتیجه رفتار ناهمزمان در آینده برگردانده خواهد شد، جاوا اسکریپت فکر میکند، “بسیار خوب، این تابع ناهمزمان هنوز نتیجه فوری ندارد. بگذارید همینطور باشد و به پردازش دستورات زیر ادامه دهید. وقتی همه چیز انجام شد، بررسی کنید که آیا یک تابع دارد یا خیر. نتیجه.” برای اطلاعات بیشتر در مورد نحوه عملکرد پردازش ناهمزمان، می توانید به مقالات برنامه نویسی ناهمزمان در جاوا اسکریپت مراجعه کنید.
Promise چند روش استاتیک مفید برای موارد استفاده مختلف دارد، مانند all
، allSettled
، any
، و race
.
Promise.all
آرایه ای از Promises را می گیرد، یک Promise برمی گرداند و در آن قرار می گیرد fulfilled
زمانی که همه Promises در آرایه موفقیت آمیز هستند را بیان کنید. در غیر این صورت، آن را در rejected
حالت زمانی که حداقل یک Promise در آرایه با شکست مواجه شود.
Promise.allSettled
شبیه است به Promise.all
اما همیشه نتایج تمام Promises در آرایه را بدون در نظر گرفتن موفقیت یا شکست برمی گرداند. هر دو Promise.all
و Promise.allSettled
زمانی که نیاز دارید چندین تابع ناهمزمان را فوراً بدون اهمیت دادن به ترتیب نتایج اجرا کنید مفید هستند.
از سوی دیگر، Promise.race
بدون در نظر گرفتن موفقیت یا شکست، نتیجه Promise را که ابتدا تسویه شده است، برمی گرداند. Promise.any
اولین وعده انجام شده را در آرایه برمی گرداند. race
و any
زمانی مناسب هستند که چندین وعده دارید که اقدامات مشابهی را انجام می دهند و نیاز به بازگشت بین نتایج دارند.
Promise جایگزین “Callback Hell” می شود
جای تعجب است که بدانید جاوا اسکریپت قبلاً Promise نداشت. بله، درست شنیدی. این واقعیت باعث شد که Node.js نیز در روزهای اولیه Promise نداشته باشد، و این بزرگترین پشیمانی است که سازنده Node.js دارد.
پیش از این، تمام وظایف ناهمزمان از طریق تماس پاسخ داده می شد. ما یک تابع callback تعریف می کنیم تا نتیجه را در آینده مدیریت کند.
به عنوان مثال، یک تابع درخواست ناهمزمان اولیه بود XMLHttpRequest
. نتیجه را از طریق تماس پاسخ داد.
function reqListener() {
console.log(this.responseText);
}
const req = new XMLHttpRequest();
req.addEventListener('load', reqListener);
req.open('GET', 'https://example.com');
req.send();
reqListener
تابعی است که زمانی فراخوانی می شود که نتیجه ای از درخواست به وجود داشته باشد https://example.com
. مدیریت رفتار ناهمزمان با تماسهای برگشتی مشکلاتی را به همراه داشت، از جمله آنچه که معمولاً به عنوان “جهنم پاسخ به تماس” شناخته میشود.
به عنوان مثال، یک تابع ناهمزمان fnA
یک تابع تماس با دو پارامتر دریافت می کند: data
نشان دهنده نتیجه موفقیت آمیز و err
نشان دهنده خطا توابع مشابه شامل fnB
، fnC
و غیره اگر بخواهیم این توابع پردازشی را با هم ترکیب کنیم، باید آنها را در داخل یکدیگر لانه کنیم.
fnA((data1, err1) => {
fnB((data2, err2) => {
fnC((data3, err3) => {
....
});
});
});
هنگامی که تعداد بیش از حد تماسهای برگشتی به این شکل نوشته میشود، کد ما به معنای واقعی کلمه به “جهنم پاسخ به تماس” تبدیل میشود. آشفته می شود و مانع از روند خواندن و درک می شود.
Promise برای ارائه یک رویکرد جدید برای مدیریت رفتار ناهمزمان معرفی شد. ما هنوز از تماسهای برگشتی استفاده میکنیم، اما میتوانیم «جهنم» را با اتصال متوالی همه چیز محدود کنیم then
.
fnA()
.then(data => fnB(data))
.then(data => fnC(data))
...
.catch(err => console.log(err));
پس از آن، بسیاری از توابع مبتنی بر تماس ناهمزمان با استفاده از آن بازنویسی شدند new Promise
. در Node.js، ما حتی یک ماژول داخلی به نام util.promisify مخصوص این تبدیل داریم. تماسهای برگشتی همچنان پشتیبانی میشوند، اما با مزایایی که Promise به ارمغان میآورد، بسیاری از کتابخانههای جدید از Promise به عنوان پیشفرض برای مدیریت رفتار ناهمزمان استفاده میکنند.
Promise in Loops
هنگام استفاده از Promise بدون درک کامل ماهیت آن، اشتباهات تاسف بار زیادی وجود دارد و یکی از آنها مدیریت عملیات ناهمزمان متوالی در یک حلقه است.
فرض کنید باید بین 5 صفحه تکرار کنید، تماس های API صفحه بندی شده را از 1 تا 5 انجام دهید و مقادیر بازگشتی را به ترتیب در یک آرایه به هم متصل کنید.
const results = [];
for (let i = 1; i <= 5; i++) {
fetch(`https://example.com?page=${i}`)
.then(response => response.json())
.then(data => results.push(data));
}
کد بالا یک حلقه برای واکشی داده ها از صفحه 1 تا صفحه 5 ایجاد می کند و داده های برگشتی به داخل results
آرایه. در نگاه اول، نتیجه آرایه ای از داده ها به ترتیب از صفحه اول تا صفحه آخر خواهد بود. با این حال، در واقعیت، هر بار که اجرا می شود، متوجه خواهید شد که داده ها در results
به صورت تصادفی سفارش داده شده است
یاد آوردن fetch
، یک Promise را برمی گرداند که نشان دهنده یک نتیجه آینده است… در حالی که for
سعی میکند در سریعترین زمان ممکن از آن عبور کند، میتوانید آن را به عنوان همه 5 مورد در نظر بگیرید fetch
فراخوانی و شروع “تقریبا فورا” دستورات. در این مرحله، سرعت CPU اهمیت کمتری دارد – این سرعت شبکه است که تعیین می کند کدام یک fetch
دستور اولین نتیجه را دارد. به محض رسیدن به نتیجه، بلافاصله آن را به داخل هل می دهد results
، در نتیجه داده ها به صورت تصادفی اضافه می شوند.
برای حل این مشکل راه های مختلفی وجود دارد. مثلا:
const results = [];
fetch('https://example.com?page=1')
.then(response => response.json())
.then(data => {
results.push(data);
return fetch('https://example.com?page=2');
})
.then(response => response.json())
.then(data => {
results.push(data);
return fetch('https://example.com?page=3');
})
...
این احمقانه است و هیچکس اینگونه کد نمی نویسد. تصور کنید اگر 1000 صفحه وجود داشته باشد چه می شود؟ این یک شوخی است، اما مثال بالا سعی می کند این ایده را نشان دهد که چگونه باید قبل از ادامه درخواست بعدی منتظر تکمیل درخواست قبلی بود.
Bluebird یک کتابخانه Promise بسیار خوب است که بسیاری از توابع کاربردی را برای سهولت کار با توابع ناهمزمان ارائه می دهد.
مثال بالا را می توان با استفاده از بازنویسی کرد each
عملکرد ارائه شده توسط Bluebird.
const results = [];
Promise.each([1, 2, 3, 4, 5], (i) => {
return fetch(`https://example.com?page=${i}`)
.then(response => response.json())
.then(data => results.push(data));
});
Async/Await – The Missing Piece of Promise؟
برای ایجاد یک Promise از سینتکس استفاده می کنیم new Promise
، اما با async/wait، ما به سادگی یک را اعلام می کنیم async
تابع.
async function isEven(x) {
if (x % 2 === 0) {
return true;
} else {
throw new Error('x is not even');
}
}
در حالی که Promise استفاده می کند then
برای رسیدگی به نتیجه بازگشت در نقطه ای در آینده، async/wait به سادگی استفاده است await
.
const result = await fetch('https://example.com');
const resultJSON = await result.json();
با شروع از Node.js نسخه 14.8، ما ویژگی انتظار سطح بالا را داریم، به این معنی await
تماس ها دیگر محدود به داخل یک نمی باشد async
تابع؛ آنها را می توان در خارج نیز استفاده کرد. قبل از آن، await
فقط می تواند در داخل یک استفاده شود async
تابع.
async function getData() {
const result = await fetch('https://example.com');
const resultJSON = await result.json();
}
در حالی که Promise استفاده می کند .catch
برای رسیدگی به خطاها، استفاده از همگام سازی/انتظار try...catch
.
async function getData() {
try {
const result = await fetch('https://example.com');
const resultJSON = await result.json();
} catch (err) {
console.log(err);
}
}
بدیهی است که async/wait به ما امکان میدهد تا کدهای ناهمزمان را بهصورت همزمان، بدون تماس و بدون نیاز به نوشتن بنویسیم. then
. ما به سادگی منتظر نتیجه با استفاده هستیم await
.
با این حال، این می تواند منجر به برخی سوء تفاهمات خطرناک مانند فراموشی شود await
کلمه کلیدی برای منتظر نتیجه یک رفتار ناهمزمان یا به اشتباه فکر کردن که یک تابع ناهمزمان یک تابع همزمان است.
async function getData() {
try {
const result = await fetch('https://example.com');
const resultJSON = await result.json();
return resultJSON;
} catch (err) {
console.log(err);
}
}
function main() {
const data = getData();
console.log(data);
}
در اصل، getData
یک Promise را برمی گرداند، بنابراین اگر قصد دریافت داده از یک تماس API باشد، برنامه فوق به درستی کار نمی کند. برای رفع این مشکل، باید آن را تغییر دهیم.
async function main() {
const data = await getData();
console.log(data);
}
بازگشت در مقابل بازگشت در انتظار
گاهی اوقات ممکن است با کدی مواجه شوید که به شکل زیر است:
async function fn() {
...
return await asyncFn();
}
کارکرد fn
در حال بازگشت است await
از تابع ناهمزمان asyncFn
. نویسنده احتمالا برای fn
تا همیشه نتیجه را برگردانیم asyncFn
، مانند await
منتظر نتیجه عملکرد ناهمزمان است که به طور موثر می چرخد fn
به یک تابع همزمان، یا به عبارت دیگر، “عدم” برگرداندن یک Promise.
متأسفانه این یک سوء تفاهم خطرناک است. در اصل، await
فقط برای استفاده در سطح بالا یا داخل یک async
عملکرد، و اگر آن است async
، باید یک Promise را برگرداند. از این رو، fn
همیشه یک وعده را برمی گرداند. پس چرا استفاده کنید return await asyncFn()
?
به سادگی استفاده کنید return asyncFn()
می تواند شما را کمی از فشار دادن کلید صرفه جویی کند، زیرا تفاوت قابل توجهی در مقایسه با آن وجود ندارد return await asyncFn()
. تغییر بزرگ زمانی اتفاق می افتد که وجود داشته باشد try...catch
که در return
.
بیایید دو تابع زیر را در نظر بگیریم:
async function rejectionWithReturnAwait () {
try {
return await Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
async function rejectionWithReturn () {
try {
return Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
اولین تابع، rejectionWithReturnAwait
، به عمد استفاده می کند return await
و هنگامی که این وعده رد شد، catch
بلوک به سرعت خطا را دریافت کرده و اجرا می کند return 'Saved!'
بیانیه. این بدان معناست که تابع یک Promise حاوی رشته “ذخیره شده” را برمی گرداند.
متقابلا، rejectionWithReturn
، بدون await
، سعی در رد الف دارد Promise.reject(new Error())
، و catch
بلوک هرگز اجرا نمی شود.
منابع: