من میدانم که هیچ نمیدانم.
بررسی پرامیسها (Promise) در جاوااسکریپت
یکی از پراستفادهترین ویژگیهای جاوااسکریپت که از ES6 به این زبان اضافه شده، پرامیسها هستن که توی این قسمت میخوایم مفصل اونها رو بررسی کنیم
سلام دوستان. Promise که برای مدیریت کردن عملیات ناهمگام استفاده میشه، یکی از کاربردیترین ویژگیهای جاوااسکریپت هست که از ES6 به این زبان اضافه شده. توی این قسمت میخوایم این موضوع کاربردی رو بررسی کنیم.
توی این قسمت یاد میگیریم که:
- مشکل کجاست
- پرامیس چیه
- resolve و reject چی هستن
- اگه عملیات موفقیتآمیز بود
- اگه عملیات با خطا مواجه شد
- پاس دادن اطلاعات به Handler ها
- Handler های پشت سر هم
- وضعیتهای پرامیس
- متد finally
- مثال دنیای واقعی
مشکل کجاست؟ ?
ابتدا با مفهومی به اسم عملیات ناهمگام آشنا بشیم:
عملیات ناهمگام به عملیاتی گفته میشه جدا از روند اصلی برنامه پردازش میشه
توی جاوااسکریپت، تابع setTimeout
یکی از معروفترین مثالها برای درک مفهوم عملیات ناهمگام هست. با این تابع میتونیم قطعه کدی رو بعد از یک تأخیر معین اجرا کنیم. کد زیر رو در نظر بگیرین:
setTimeout(function () {
alert(1); // 1
}, 2000);
alert(2); // 2
ورودی اول setTimeout
یک تابع هست و شامل عملیاتی میشه که میخوایم اون رو با تأخیر انجام بدیم. ورودی دوم مدت زمان تأخیر به میلیثانیه هست. اینجا 2000 یعنی 2 ثانیه.
بعد از اجرای کد میبینیم که ابتدا alert
خط ۵ نمایش داده میشه و بعد از ۲ ثانیه alert
خط ۲. این یعنی یک عملیات ناهمگام داریم. شاید انتظار داشتیم که جاوااسکریپت خط به خط کدهای ما رو بررسی کنه و خروجیها رو نمایش بده. اما وقتی که جاوااسکریپت به قسمتی از کد رسید که شامل یک عملیات ناهمگام بود، منتظر نتیجه این قسمت نموند و به ادامه بررسی خطهای بعدی پرداخت.
توی جاوااسکریپت، یک سری عملیات مثل اتصال به یک منبع خارجی با Ajax و WebSocket و یا خوندن و نوشتن اطلاعات توی دیتابیس و هارددیسک، جدا از روند اصلی برنامه اجرا میشن. این یعنی عملیات ناهمگام.
چطوری میتونیم بعد از به پایان رسیدن یک عملیات ناهمگام کاری رو انجام بدیم؟ مثلاً توی کد بالا ابتدا صبر کنیم کار تابعی که به setTimeout
پاس دادیم تموم بشه و بعد alert
با مقدار 2 رو بگیریم؟ باید راهی باشه که بتونیم عملیات ناهمگام رو مدیریت کنیم. با پرامیسها این کار رو میتونیم انجام بدیم ?
پرامیس چیه؟
پرامیسها برای مدیریت کردن عملیات ناهمگام استفاده میشن. نحوه نوشتن یک پرامیس خیلی راحته. ابتدا با ساختار اون بشیم و بعد مثال بالا رو با پرامیس خواهیم نوشت:
new Promise(function (resolve, reject) {
// do something ...
});
همونطور که میبینیم، یک پرامیس با ساختن یک نمونه از کلاس Promise با استفاده از کلمهکلیدی new ساخته میشه. کلاس پرامیس فقط یک آرگومان میگیره و اون هم یک تابع هست. این تابع که توی اون باید عملیات ناهمگامِمون رو بنویسیم، بلافاصله وقتی یک پرامیس ساخته شد اجرا میشه.
حالا بیاین مشکلی که توی مثالمون داشتیم رو با پرامیسها حل کنیم تا با ساختار اون بیشتر آشنا بشیم.
const welcome = new Promise(function (resolve, reject) {
setTimeout(() => {
alert(1);
resolve();
}, 2000);
});
welcome.then(function () {
alert(2);
});
با اجرای این کد ابتدا با یک تاخیر ۲ ثانیهای alert
با مقدار 1 میگیریم و بعد 2. اینطوری تونستیم عملیات ناهمگام رو مدیریت و به قول معروف هندل کنیم ?
حالا چیزهایی که نوشتیم رو بررسی کنیم.
resolve
و reject
چی هستن؟
همونطور که میبینیم تابعی که به پرامیس پاس دادیم دو تا پارامتر میگیره که اولی resolve
و دومی reject
هست:
new Promise(function (resolve, reject) {
// ...
هر دو پارامتر باید به شکل یک تابع توی پرامیس صدا زده بشن. یعنی:
new Promise(function (resolve, reject) {
resolve();
// or
reject();
});
ما توی پرامیس باید به جاوااسکریپت بگیم که کارِ ما به پایان رسید. چون جاوااسکریپت خودش متوجه به پایان رسیدن این عملیات نمیشه. وقتی عملیات ما به پایان رسید، اگر خروجی کار موفقیتآمیز بود، باید resolve
رو صدا بزنیم و اگر خطا داشت reject
.
اگه عملیات موفقیتآمیز بود
توی پرامیسی که نوشتیم توی خط ۴ resolve
رو صدا زدیم به این معنی که عملیات ما با موفقیت به پایان رسیده:
const welcome = new Promise(function (resolve, reject) {
setTimeout(() => {
alert(1);
resolve();
}, 2000);
});
چرا باید به جاوااسکریپت بگیم که عملیات ما به پایان رسید؟ برای اینکه بتونیم بعد از به پایان رسیدن عملیات ناهمگام، کار مد نظرمون رو انجام بدیم. توی مثالی که داشتیم، کار مد نظرمون رو به این صورت انجام دادیم:
welcome.then(function () {
alert(2);
});
همونطور که میبینیم از متغیر welcome
که مقدار اون یک پرامیس هست، یک متد به اسم then
رو صدا زدیم و به اون تابعی رو پاس دادیم. این تابع بلافاصله زمانی اجرا میشه که ما توی پرامیس resolve
رو صدا زده باشیم! پس کاری که میخوایم بعد از به پایان رسیدن عملیات ناهمگام اجرا بشه رو با then
انجام میدیم.
اگه عملیات با خطا مواجه شد
اگه عملیات ناهمگام با خطا مواجه شد، باید توی پرامیس reject
رو صدا بزنیم:
const welcome = new Promise(function (resolve, reject) {
const status = 'failed';
setTimeout(() => {
if (status === 'failed') {
reject();
}
}, 2000);
});
این کار به جاوااسکریپت میگه که عملیات ناهمگاممون موفقیتآمیز نبود. اگه بخوایم توی برنامه، reject
شدن یک پرامیس رو مدیریت کنیم از متد catch
که مشابه then
هست استفاده میکنیم:
const welcome = new Promise(function (resolve, reject) {
const status = 'failed';
setTimeout(() => {
if (status === 'failed') {
reject();
}
}, 2000);
});
welcome.catch(function () {
alert("Promise failed!");
});
همونطور که میبینیم، توی catch
یک تابع تعریف کردیم. این تابع بلافاصله بعد از اینکه پرامیس reject
شد اجرا میشه.
به متدهای then
و catch
میگیم Handler بهمعنی کنترلکننده نتیجه عملیات ناهمگام.
ما برای مدیریت کردن خطای پرامیس میتونیم بجای متد catch
از آرگومان دوم متد then
کمک بگیریم:
const welcome = new Promise(function (resolve, reject) {
reject();
});
welcome.then(
function () { alert("Succeeded") },
function () { alert("Failed") }
);
تابعی که به آرگومان دوم پاس دادیم فقط زمانی اجرا میشه که reject
صدا زده بشه. در واقع متد catch
مشابه کد زیر هست:
myPromise.then(null, function () {
});
پاس دادن اطلاعات به Handler ها
هر چیزی که به توابع resolve
و reject
پاس بدیم، توی متدهای then
و catch
قابل دسترس هست:
const welcome = new Promise(function (resolve, reject) {
resolve("Hello!");
});
welcome.then(function (response) {
alert(response); // Hello!
});
Handler های پشت سر هم
یک پرامیس وقتی که resolve
میشه میتونه چندین Handler داشته باشه. به این معنی که بتونیم کارهای مختلفی رو انجام بدیم:
const welcome = new Promise(function (resolve, reject) {
resolve(1);
});
welcome.then(res => alert(res + 1)); // 2
welcome.then(res => alert(res + 2)); // 3
welcome.then(res => alert(res + 3)); // 4
Handler های پشت سر هم میتونن به همدیگه بچسبن:
const welcome = new Promise(function (resolve, reject) {
resolve(1);
});
welcome.then().then().then();
توی چنین شرایطی، خروجی هر then
به then
بعدی پاس داده میشه:
const welcome = new Promise(function (resolve, reject) {
resolve("Hello");
});
welcome
.then(res1 => res1.split("")) // ["H", "e", "l", "l", "o"]
.then(res2 => res2.reverse()) // ["o", "l", "l", "e", "H"]
.then(res3 => res3.join("")) // olleH
.then(res4 => alert(res4)); // olleH
وضعیتهای پرامیس
دیدیم که نتیجه پرامیس یا موفقیتآمیز هست یا رد شده. در واقع یک پرامیس همیشه سه حالت داره: Pending - Rejected - Fulfilled
۱. حالت Pending
این حالت یعنی حالت انتظار. پرامیس در ابتدا و وقتی که ساخته میشه، در این حالت قرار داره:
const asyncOpr = new Promise(function(resolve, reject) {
// no resolve or reject
})
console.log(asyncOpr);
این حالت تا زمانی که resolve
یا reject
صدا زده نشه باقی میمونه. کد را اجرا کنید و کنسول رو ببینین.
۲. حالت Fulfilled
حالت پرامیس وقتی resolve
رو صدا بزنیم در حالت Fulfilled یعنی برآورده شده و موفقیتآمیز بودن قرار میگیره:
new Promise(function (resolve, reject) {
setTimeout(function() {
resolve();
}, 1000);
});
توی این کد، پرامیس بعد از ۱ ثانیه توی وضعیت Fulfilled قرار میگیره چون resolve
رو صدا زدیم.
۳. حالت Rejected
حالت پرامیس وقتی reject
رو صدا بزنیم در حالت Rejected قرار میگیره. یعنی عملیات ناهمگام موفقیتآمیز نبود:
new Promise(function (resolve, reject) {
setTimeout(function() {
reject();
}, 1000);
});
توی این کد، پرامیس بعد از ۱ ثانیه توی وضعیت Rejected قرار میگیره چون reject
رو صدا زدیم.
نکتهای که باید در نظر داشته باشیم اینه که پرامیس فقط میتونه توی یک کدوم از حالتهای Fulfilled و یا Rejected قرار بگیره:
new Promise(function (resolve, reject) {
resolve();
reject();
});
توی این کد، با توجه به اینکه resolve
زودتر صدا زده شده، reject
کلاً نادیده گرفته میشه. پس catch
هم اجرا نخواهد شد:
const welcome = new Promise(function (resolve, reject) {
resolve();
reject(); // ignored
});
welcome.then(() => {
alert("Success!"); //Success!
});
welcome.catch(() => {
alert("Failed!");
});
اما اگه reject
زودتر صدا زده بشه، then
نادیده گرفته میشه.
متد finally
اگه کاری داریم که میخوایم در هر دو صورت (هم resolve
شدن و هم reject
شدن) انجام بدیم، از یک Handler دیگه به اسم finally
استفاده میکنیم:
const input = prompt("Enter 1");
const welcome = new Promise((resolve, reject) => {
if (input == 1) {
resolve("Succeeded!");
} else {
reject("Failed!");
}
});
welcome.then(alert);
welcome.catch(alert);
welcome.finally(() => {
clearCache();
});
function clearCache() {
alert("Program cache cleared!");
}
همونطور که میبینیم کدی که توی خط ۱۵ نوشتیم تحت هر شرایطی اجرا میشه. تابعی که به finally
پاس دادیم هیچ آرگومانی قبول نمیکنه. چون نتیجه عملیات براش مهم نیست.
بدون استفاده از finally
کد بالا رو باید اینطوری مینوشتیم:
welcome.then(
function (success) {
clearCache();
},
function(error) {
clearCache();
}
);
اما این راه مناسب نیست چون کد تکراری داریم مینویسیم.
مثال دنیای واقعی
توی کد زیر تابعی داریم که با اون میتونیم اسکریپتهای مختلفی (مثل جیکوئری، Axios و ...) رو از طریق URL به صفحه اضافه کنیم:
function loadScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = path;
script. = () => resolve(path);
script. = () => reject(new Error('Whoops'));
document.head.append(script);
});
}
توی این تابع یک پرامیس نوشتیم که توی اون اسکریپت رو به صفحه اضافه میکنیم و به محض اینکه اسکریپت دانلود بشه، پرامیس resolve
میشه. توی تابع loadScript
یک پرامیس داره return
میشه. پس خروجی این تابع یک پرامیس هست.
حالا میخوایم کتابخونه Axios که محبوبترین ابزار برای ارسال درخواستهای Ajax هست رو به صفحه اضافه و از اون استفاده کنیم:
const loadAxios = loadScript("https://unpkg.com/axios/dist/axios.min.js");
الان مقدار متغیر loadAxios
یک پرامیس هست و به محض اینکه اسکریپت مد نظرمون لود بشه میخوایم از اون استفاده کنیم. از متدهای then
یا catch
به صورت زیر استفاده میکنیم:
loadAxios.then(() => {
// Axios is loaded
axios.get(path);
});
همونطور که میدونیم کتابخونه Axios هم مبتنی بر پرامیسها هست. یعنی نتیجهٔ درخواستهایی که با اون میفرستیم رو میتونیم به شکل پرامیسها مدیریت کنیم. توی این مثال میخوایم به یک آدرس خارجی یک درخواست Ajax بزنیم و نتیجه رو به کاربر نشون بدیم:
const loadAxios = loadScript("https://unpkg.com/axios/dist/axios.min.js");
loadAxios.then(() => {
axios.get("https://randomuser.me/api/?inc=name&results=3")
.then(response => {
if (response.status === 200) {
const users = response.data.results;
for (const {name} of users) {
alert(`Welcome ${name.title} ${name.first} ${name.last}! ?`);
}
}
});
});
function loadScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = path;
script. = () => resolve(path);
script. = () => reject(new Error('Whoops'));
document.head.append(script);
});
}
با اجرای کد میبینیم که ابتدا Axios به صفحه اضافه میشه و بعد با استفاده از اون، درخواستی به آدرس مد نظرمون میزنیم تا اطلاعات رو به کاربر نشون بدیم. توی این برنامه ۲ بار از پرامیسها استفاده کردیم ??
خب دوستان این قسمت هم به پایان رسید و امیدوارم استفاده کرده باشید. برای ادامه آموزش به قسمتهای بعدی برین. روزتون خوش ? ?
Resources :
مطلبی دیگر از این انتشارات
بازی دنیای کشاورزان، کشاورزی با NFT
مطلبی دیگر از این انتشارات
«شفافیت»، گذر از مزیت رقابتی به الزام استراتژیک!
مطلبی دیگر از این انتشارات
پروژه های بلاک چینی با کدام زبانهای برنامه نویسی نوشته می شوند؟