خیلی وقتها در برنامهنویسی نیاز داریم که دو یا چند عملیات ناهمگام (Asynchronous) را پشت سر هم و به صورت زنجیرهوار اجرا کنیم؛ طوری که هر عملیات، زمانی شروع شود که عملیات قبلی با موفقیت به پایان رسیده باشد و بتواند از نتیجه مرحله قبل استفاده کند.
در گذشته، اگر میخواستیم چند عملیات ناهمگام را پشت سر هم انجام بدهیم، کدهایمان به شکل تودرتو و عجیبی در میآمد که به آن جهنم کالبکها (Callback Hell) میگفتند. به این نمونه نگاه کنید:
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
اما با آمدن پرامیسها، ما این کار را خیلی راحتتر و به کمک یک زنجیره پرامیس (Promise Chain) انجام میدهیم. طراحی این ابزار به شدت هوشمندانه است؛ چون به جای اینکه توابع کالبک را به زور به عنوان ورودی به شکم یک تابع پاس بدهیم، آنها را خیلی شیک به خودِ شیء پرامیسِ برگشتدادهشده متصل میکنیم.
راز اصلی این جادو اینجاست: تابع .then() خودش یک پرامیس جدید برمیگرداند که با پرامیس قبلی متفاوت است:
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
این پرامیس دوم (promise2) نه تنها نشاندهنده تمام شدن تابع doSomething() است، بلکه تمام شدن خود successCallback یا failureCallback که به آن پاس دادهاید را هم نشان میدهد. حالا اگر این کالبکها، خودشان توابع ناهمگام دیگری باشند که یک پرامیس برمیگردانند، هر کالبک جدیدی که به promise2 اضافه شود، در صفِ پشتِ پرامیسِ برگشتیِ آنها قرار میگیرد.
💡 یک قالب آماده برای تمرین: اگر دوست دارید یک مثال واقعی داشته باشید که بتوانید با آن بازی کنید و تستش کنید، میتوانید از قالب زیر برای ساختن هر تابعی که یک پرامیس برمیگرداند استفاده کنید:
function doSomething() { return new Promise((resolve) => { setTimeout(() => { // کارهای دیگری که باید قبل از اتمام پرامیس انجام شوند console.log("Did something"); // مقداری که پرامیس با موفقیت برمیگرداند resolve("https://example.com/"); }, 200); }); }(جزئیات پیادهسازی این موضوع را در بخش «ساخت یک پرامیس بر اساس یک کالبک قدیمی» بررسی خواهیم کرد).
با استفاده از این الگو، شما میتوانید زنجیرههای طولانیتری از پردازش را بسازید که در آن، هر پرامیس نشاندهنده اتمام یک مرحله ناهمگام در طول این زنجیره است.
علاوه بر این، جالب است بدانید که آرگومانهای ورودی متد .then کاملاً اختیاری هستند و نوشتن .catch(failureCallback) در واقع شکل کوتاهشدهی .then(null, failureCallback) است. پس اگر کد مدیریت خطای شما برای تمام مراحل زنجیره یکسان است، میتوانید خیلی راحت آن را به انتهای زنجیره متصل کنید:
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
اگر طرفدار توابع پیکانی (Arrow Functions) هستید، همین کد بالا را میتوانید خیلی خلاصهتر و زیباتر هم بنویسید:
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
(نکته: توابع پیکانی قابلیت بازگشت خودکار یا همان implicit return دارند؛ یعنی عبارت () => x دقیقاً شکل کوتاهشدهی () => { return x; } است).
توابع doSomethingElse و doThirdThing میتوانند هر مقداری را برگردانند. اگر آنها یک پرامیس برگردانند، زنجیره ابتدا منتظر میماند تا آن پرامیس تعیین تکلیف (Settle) شود و کالبک بعدی، خودِ مقدارِ موفقیت را دریافت میکند، نه خودِ شیء پرامیس را.
returnیک نکته حیاتی که باید همیشه یادتان باشد این است که همیشه پرامیسها را از داخل کالبکهای .then() برگردانید (return کنید)؛ حتی اگر آن پرامیس همیشه به مقدار undefined ختم شود. اگر کالبک قبلی یک پرامیس را شروع کند اما آن را return نکند، دیگر هیچ راهی برای پیگیری وضعیت اتمام آن وجود نخواهد داشت و به این پرامیس اصطلاحاً «شناور» (Floating) میگویند.
بیا با مثال ببینیم چه اتفاقی میافتد:
❌ حالت اشتباه (بدون کلمه کلیدی return):
doSomething()
.then((url) => {
// کلمه کلیدی `return` قبل از fetch(url) فراموش شده است!
fetch(url);
})
.then((result) => {
// مقدار result اینجا undefined است، چون در مرحله قبل هیچ چیزی return نشد.
// دیگر هیچ راهی وجود ندارد که بفهمیم fetch چه چیزی برگردانده یا اصلاً موفقیتآمیز بوده یا نه.
});
✅ حالت درست (همراه با کلمه کلیدی return): با برگرداندن نتیجهی فرآیند fetch (که خودش یک پرامیس است)، ما هم میتوانیم فرآیند انجام شدنش را ردیابی کنیم و هم وقتی کارش تمام شد، مقدارش را تحویل بگیریم:
doSomething()
.then((url) => {
// کلمه کلیدی `return` اضافه شد
return fetch(url);
})
.then((result) => {
// حالا result یک شیء واقعی از نوع Response است
});
وضعیت پرامیسهای شناور زمانی خطرناکتر میشود که با شرایط مسابقه روبرو شوید. اگر پرامیس از آخرین کالبک برگردانده نشود، کالبک .then() بعدی خیلی زودتر از موعد صدا زده میشود و هر مقداری که بخواند ممکن است ناقص یا اشتباه باشد:
❌ نمونه اشتباه و ناقص:
const listOfIngredients = [];
doSomething()
.then((url) => {
// کلمه کلیدی `return` قبل از fetch(url) جا افتاده است.
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// این آرایه همیشه خالی [] چاپ میشود، چون هنوز عملیات fetch تمام نشده است!
});
✅ راهکار اصلاحشده: به عنوان یک قاعده کلی و تجربی، هر زمان که در عملیات خود با یک پرامیس مواجه شدید، آن را برگردانید (return کنید) و مدیریت آن را به دست کالبک .then() بعدی بسپارید:
const listOfIngredients = [];
doSomething()
.then((url) => {
// حالا کلمه کلیدی `return` قبل از fetch قرار گرفته است
return fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// حالا listOfIngredients به درستی شامل دادههای دریافتی از fetch خواهد بود.
});
حتی بهتر از این، شما میتوانید این زنجیره تودرتو را صاف (Flatten) کرده و به یک زنجیره واحد و یکدست تبدیل کنید. این کار هم ظاهر کد را سادهتر میکند و هم مدیریت خطاها را به شدت راحتتر میسازد (جزئیات بیشتر را در بخش «تودرتویی یا Nesting» بررسی میکنیم):
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});
async / awaitاستفاده از ساختار async/await به شما کمک میکند کدهایی بنویسید که بسیار شهودیتر هستند و شباهت زیادی به کدهای همگام (Synchronous) و معمولی دارند. در زیر، همان مثال بالا را اینبار با استفاده از تکینیک async/await میبینیم:
async function logIngredients() {
const url = await doSomething();
const res = await fetch(url);
const data = await res.json();
listOfIngredients.push(data);
console.log(listOfIngredients);
}
ببینید چقدر کد تمیز شد! این کد دقیقاً شبیه کدهای معمولی و خط به خط کار میکند، با این تفاوت که کلمه کلیدی await قبل از پرامیسها قرار گرفته است. تنها چالش یا به اصطلاح وجه مصالحهای که این روش دارد این است که ممکن است گاهی فراموش کنید کلمه await را بنویسید؛ مشکلی که معمولاً فقط زمانی متوجه آن میشوید که با خطای عدم تطابق تایپ مواجه شوید (مثلاً وقتی که میخواهید از یک پرامیس به عنوان یک مقدار معمولی استفاده کنید).
یادتان باشد که ساختار async/await کاملاً روی دوش پرامیسها سوار شده است؛ به عنوان مثال، تابع doSomething() در اینجا دقیقاً همان تابعی است که بالاتر استفاده کردیم، بنابراین برای تغییر دادن کدهای پرامیس قدیمی به کدهای مدرنِ async/await، به کمترین میزان بازنویسی (Refactoring) نیاز خواهید داشت. (برای اطلاعات بیشتر درباره این ساختار میتوانید به مراجع توابع async و await مراجعه کنید).
📌 نکته پایانی: ساختار
async/awaitدقیقاً همان رفتار و ویژگیهای همزمانیِ زنجیرههای پرامیس معمولی را دارد. استفاده از کلمه کلیدیawaitدر داخل یک تابع async، کل برنامه شما را متوقف نمیکند، بلکه فقط بخشهایی را متوقف نگه میدارد که مستقیماً به آن مقدار وابسته هستند. بنابراین، سایر کارهای ناهمگام برنامه شما همچنان میتوانند به راحتی در زمان انتظارِawaitبه کار خود ادامه دهند.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript