برنامه نویسی ناهمگام جاوا اسکریپت با Promise ها — راهنمای کاربردی
Promise-ها قابلیت نسبتاً جدیدی در زبان جاوا اسکریپت هستند که امکان به تأخیر انداختن برخی گزینهها تا پایان یافتن برخی کارها را میدهند. همچنین با استفاده از Promise میتوان در صورت ناموفق بودن یک کار به آن پاسخ داد. این وضعیت برای راهاندازی یک توالی از عملیات ناهمگام که به صورت صحیحی کار میکنند کاملاً مفید است. در این مقاله به بررسی روش برنامه نویسی ناهمگام جاوا اسکریپت و طرز کار Promise میپردازیم، شیوه استفاده از آنها در API-های وب را بررسی میکنیم و با شیوه نوشتن آنها آشنا خواهیم شد.
پیشنیازهای مطالعه این مقاله داشتن سواد مقدماتی کامپیوتر و درک معقولی از مبانی جاوا اسکریپت است. هدف از این مقاله نیز درک Promise-ها و روش استفاده از آنها است. برای مطالعه قسمت قبلی این مجموعه مطلب آموزشی میتوانید روی لینک زیر کلیک کنید:
Promise-ها چه هستند؟
ما قبلاً در برخی مطالب دیگر مجله فرادرس ساختار Promise را در جاوا اسکریپت مورد بررسی قرار دادیم. در این مقاله به بررسی دقیق این ساختار نسبتاً جدید میپردازیم.
Promise اساساً شیئی است که یک حالت میانجی عملیات را نمایش میدهد. در واقع Promise نوعی نتیجه است که در نقطهای از زمان در آینده رخ میدهد. تضمینی وجود ندارد که عملیات دقیقاً چه زمانی پایان مییابد و نتیجه بازگشت مییابد، اما این تضمین وجود دارد که وقتی نتیجه آماده شد یا Promise ناموفق باشد، کد ارائه شده اجرا میشود تا با نتیجه بازگشتی کاری انجام دهد یا در صورت عدم موفقیت وضعیت مربوطه را مدیریت کند.
به طور کلی ما علاقهای به مقدار زمانی که طول میکشد تا یک عملیات ناهمگام پایان یابد و نتیجه را بازگشت دهد، نداریم و بیشتر علاقهمندیم که بتوانیم به نتیجه بازگشتی، هر زمان که آماده شود پاسخ دهیم. البته این که Promise باعث میشود اجرای بقیه کدها مسدود نشود هم بسیار عالی است.
یکی از رایجترین کاربردهای Promise-ها در آن دسته از Api-های وب است که Promise بازگشت میدهند. یک اپلیکیشن تماس ویدئویی فرضی را در نظر بگیرید. این اپلیکیشن پنجرهای دارد که لیستی از دوستان کاربر را نمایش میدهد و کاربر در آن میتواند با کلیک کردن روی دکمه کنار هر کاربر شروع به تماس با وی بکند.
دستگیره دکمه اقدام به فراخوانی ()getUserMedia میکند تا به دوربین و میکروفن کاربر دسترسی پیدا کند. از آنجا که ()getUserMedia باید مطمئن شود که کاربر مجوز دسترسی به این امکانات گوشی را دارد از کاربرمی خواهد که انتخاب کند میخواهد از کدام میکروفن یا دوربین استفاده کند، ممکن است تا زمانی که کاربر این میکروفن و دوربین را انتخاب نکرده است و یا حتی تا زمانی که آنها عملیاتی نشدهاند مسدود شود. به علاوه کاربر ممکن است بیدرنگ به این درخواستهای مجوز پاسخ ندهد و این وضعیت مدت زمان زیادی طول بکشد.
از آنجا که فراخوانی ()getUserMedia از نخ اصلی مرورگر اجرا میشود، کل مرورگر مسدود میشود تا این که ()getUserMedia بازگشت یابد. بدیهی است که این گزینه قابل قبول نیست. در واقع بدون وجود Promise همه چیز در مرورگر تا زمانی که کاربر تصمیم بگیرد در مورد میکروفن و دوربین میخواهد چه بکند از کار میافتند. بنابراین به جای انتظار برای کاربر، همچنین انتظار برای فعال شدن دستگاه مربوطه و بازگشت دادن مستقیم MediaStream برای استریم ایجاد شده از منابع منتخب، MediaStream یک Promise بازگشت میدهد که در زمان عرضه شدن آن اجرایی میشود.
بدین ترتیب کدی که اپلیکیشن تماس ویدئویی استفاده میکند، چیزی مانند زیر است:
1function handleCallButton(evt) {
2 setStatusMessage("Calling...");
3 navigator.mediaDevices.getUserMedia({video: true, audio: true})
4 .then(chatStream => {
5 selfViewElem.srcObject = chatStream;
6 chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream));
7 setStatusMessage("Connected");
8 }).catch(err => {
9 setStatusMessage("Failed to connect");
10 });
11}
این تابع کار خود را با فراخوانی به تابع ()setStatusMessage برای بهروزرسانی وضعیت نمایش یافته با عبارت «...Calling» آغاز میکند و بدین ترتیب نشان میدهد که تماس در حال برقراری است. سپس ()getUserMedia را فراخوانی میکند و تقاضای یک استریم میکند که هر دو تِرَک ویدئو و صوتی را در خود دارد. در ادامه زمانی که این تِرَک به دست آمد، یک عنصر ویدئویی تنظیم میکند تا استریم آمده از دوربین را در یک self view نمایش دهد، سپس هر کدام از ترکهای استریم را گرفته و آنها را به RTCPeerConnection از نوع WebRTC اضافه میکند تا اتصال به کاربر دیگر را نمایش دهد. در نهایت وضعیت نمایش یافته به صورت «Connected» بهروزرسانی میشود.
اگر ()getUserMedia موفق نباشد، بلوک کد catch اجرا میشود. در این بلوک از ()setStatusMessage برای بهروزرسانی وضعیت نمایش یافته جهت نمایش بروز خطا استفاده میشود.
نکته مهم این است که ()getUserMedia تقریباً به صورت بیدرنگ بازگشت مییابد، هر چند استریم دوربین هنوز به دست نیامده باشد. حتی اگر تابع ()handleCallButton به کدی که آن را فراخوانی کرده است بازگشت یافته باشد، زمانی که ()getUserMedia کار خود را تمام کند، دستگیره ارائه شده را فراخوانی میکند. تا زمانی که اپلیکیشن فرض نمیکند استریم کردن آغاز یافته است، میتواند به اجرای خود ادامه دهد.
مشکل Callback-ها
برای توضیح کامل این که چرا Promise-ها چیز خوبی هستند، بهتر است ابتدا در مورد سبک کدنویسی قدیمی Callback صحبت کنیم و این که چرا مشکلزا هستند.
- به عنوان قیاس یک پیتزا را تصور کنید. برای تهیه موفق یک پیتزا باید مراحل خاصی طی شوند که اجرای خارج از ترتیب آنها شاید هیچ معنایی نداشته باشد. همچنین اگر صبر نکنیم یک مرحله تمام شود و مستقیماً به سراغ مرحله بعد برویم، کار به درستی پیش نخواهد رفت.
- ابتدا نوع پیتزا را انتخاب میکنید، در صورتی که در این خصوص مردد باشید ممکن است تصمیمگیری کمی طول بکشد و حتی اگر نتوانید تصمیم خود را نهایی بکنید، ممکن است تصمیم بگیرید غذای پُرسی بخورید.
- سپس سفارش خود را ارائه میکنید. آماده شدن پیتزا ممکن است کمی طول بکشد و در صورتی که رستوران مواد لازم برای پخت آن را نداشته باشد، ممکن است سفارشتان ناموفق باشد.
- در ادامه در صورت آماده شدن، پیتزا را میخورید. این اتفاق نیز در صورتی که کیف پول خود را فراموش کرده باشید و نتوانید هزینه پیتزا را بپردازید ممکن است ناموفق باشد.
در روش callbacks به سبک قدیم، یک بازنمایی شبه کد از کارکرد فوق میتواند به صورت زیر باشد:
1chooseToppings(function(toppings) {
2 placeOrder(toppings, function(order) {
3 collectOrder(order, function(pizza) {
4 eatPizza(pizza);
5 }, failureCallback);
6 }, failureCallback);
7}, failureCallback);
این کد شلوغ و خواندن آن دشوار است و معمولاً به نام «جهنم Callback» نامیده میشود. این کد نیازمند آن است که ()failureCallback چندین بار فراخوانی شود و هر کدام مشکلات خود را دارند.
بهبودهای Promise
Promise-ها در موقعیتهایی مانند آن چه در بخش قبلی شرح دادیم، باعث میشوند که نوشتن، تجزیه و اجرای کد بسیار آسانتر شود. اگر شبه کد فوق را به جای Callback با استفاده از Promise-ها نمایش دهیم، چیزی مانند زیر خواهد بود:
1chooseToppings()
2.then(function(toppings) {
3 return placeOrder(toppings);
4})
5.then(function(order) {
6 return collectOrder(order);
7})
8.then(function(pizza) {
9 eatPizza(pizza);
10})
11.catch(failureCallback);
این کد بسیار بهتر است، دیدن آن چه که اتفاق میافتد آسانتر است. ما در این حالت تنها به یک بلوک ()catch. نیاز داریم تا همه خطاها را در آن مدیریت کنیم و موجب مسدود شدن نخ اصلی نمیشود. بدین ترتیب میتوانیم در زمانی که منتظر آماده شدن پیتزا هستیم، به بازی کردن گیم بپردازیم. در این روش تضمین میشود که هر عملیات قبل از شروع منتظر میماند تا عملیات قبلی پایان گیرد. ما میتوانیم چند اکشن ناهمگام را به هم زنجیر کنیم تا یکی پس از دیگر به این روش اجرا شوند و هر بلوک ()then. یک Promise بازگشت دهد که وقتی بلوک ()then. بازگشت مییابد اجرا شوند. با استفاده از تابعهای Arrow میتوان این کد را باز هم سادهتر ساخت:
1chooseToppings()
2.then(toppings =>
3 placeOrder(toppings)
4)
5.then(order =>
6 collectOrder(order)
7)
8.then(pizza =>
9 eatPizza(pizza)
10)
11.catch(failureCallback);
یا حتی از این هم سادهتر نوشت:
1chooseToppings()
2.then(toppings => placeOrder(toppings))
3.then(order => collectOrder(order))
4.then(pizza => eatPizza(pizza))
5.catch(failureCallback);
دلیل این که کد فوق کار میکند این است که تابعهای Arrow به صورت () => x یک اختصار معتبر برای () => { ;return x } هستند.
در نهایت شگفتی حتی میتوان کدی به صورت زیر نوشت، چون تابعها آرگومانهایشان را مستقیماً ارسال میکنند. بنابراین دیگر نیازی به لایه اضافی تابعها نیست:
1chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);
خواندن کد فوق به آن سادگی کدهای قبل نیست، و در صورتی که بلوکها پیچیدهتر از آن چه که در اینجا استفاده کردیم باشند، ممکن است قابل استفاده نباشد.
نکته: با استفاده از ساختار async/await میتوان بهبودهای بیشتری ایجاد کرد. این ساختار را در بخش بعدی این سری مقالات بیشتر بررسی میکنیم.
Promise-ها در ابتداییترین حالت خود مشابه شنوندههای رویداد هستند، اما چند تفاوت وجود دارد:
- Promise تنها یک بار میتواند موفق شود یا شکست بخورد. Promise نمیتواند دو بار موفق شود یا شکست بخورد و نمیتواند زمانی که عملیات پایان یافت، از حالت موفقیت به شکست و یا برعکس سوئیچ کند.
- اگر یک Promise موفق شود یا شکست بخورد و شما در ادامه یک Callback موفقیت یا شکست اضافه کنید، Callback صحیح فراخوانی میشود هر چند رویداد قبلاً اتفاق افتاده باشد.
توضیح ساختار مقدماتی Promise با یک مثال واقعی
درک Promise-ها حائز اهمیت است، زیرا اغلب API-های مدرن وب از آنها برای کارکردهایی استفاده میکنند که وظایف نسبتاً طولانیمدتی را اجرا میکنند. برای استفاده از فناوریهای وب مدرن باید از Promise-ها بهره گرفت. در ادامه این فصل نگاهی به شیوه نوشتن Promise-های سفارشی خواهیم داشت، اما فعلاً برخی نمونههای ساده را بررسی میکنیم که در API-های وب مشاهده میشوند.
در مثال اول، از متد ()fetch استفاده میکنیم که برای واکشی تصویری از وب استفاده میشود، متد ()blob برای تبدیل بدنه خام پاسخ واکشی شده به شیء Blob کاربرد دارد و در ادامه این blob را درون یک عنصر <img> نمایش میدهیم. این فرایند کاملاً شبیه به نمونهای است که در مثال ابتدای این سری از مقالات مشاهده کردیم، اما در اینجا به روشی نسبتاً متفاوت عمل میکنیم تا کد مبتنی بر Promise خودمان را بنویسیم.
قبل از هر چیز کد قالب خالی HTML زیر را در روی یک دایرکتوری در سیستم با نام «index.html» ذخیره کنید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>My test page</title>
6 </head>
7 <body>
8 <p>This is my page</p>
9 </body>
10</html>
این تصویر را نیز دانلود کرده و در دایرکتوری مربوطه قرار دهید:
یک عنصر <script> در انتهای <body> در کد HTML قرار دهید.
درون عنصر <script> کد زیر را اضافه کنید:
1let promise = fetch('coffee.jpg');
این کد متد ()fetch را فراخوانی کرده و URL مربوط به تصویر را از شبکه به صورت یک پارامتر واکشی میکند. آن را میتوان به عنوان یک شیء گزینه به صورت پارامتر دوم اختیاری نیز دریافت کرد، اما فعلاً از روش سادهتر استفاده میکنیم. ما شیء Promise بازگشتی از ()fetch را درون یک متغیر به نام promise ذخیره میکنیم. چنانکه پیشتر گفتیم، این شیء یک حالت میانی را نمایش میدهد که در ابتدا نه موفق و نه ناموفق است. در واقع نام رسمی این حالت «در انتظار» (pending) است.
برای پاسخدهی به تکمیل موفق عملیات در هر زمان (در این مورد زمانی که responses بازگشت یابد) متد ()then. شیء promise را فراخوانی میکنیم. Callback درون بلوک ()then. تنها زمانی اجرا میشود که فراخوانی promise با موفقیت به پایان برسد و شیء Response را بازگشت دهد. بر مبنای ادبیات Promise این اتفاق زمانی رخ میدهد که عملیات fulfilled شده باشد. بدین ترتیب شیء Response بازگشتی به صورت یک پارامتر ارسال میشود.
نکته: روش کار یک بلوک ()then. مشابه زمانی است که یک شنونده رویداد را با استفاده از ()AddEventListener به یک شیء اضافه میکنید. این بلوک تا زمانی که رویدادی رخ نداده باشد کار نمیکند. قابلتوجهترین تفاوت این است که ()then. هر بار که استفاده شود تنها یک بار اجرا میشود، در حالی که شنونده رویداد میتواند چندین بار فراخوانی شود.
ما متد ()blob را بیدرنگ روی پاسخ اجرا میکنیم تا مطمئن شویم که بدنه پاسخ به طور کامل دانلود شده است و زمانی که آماده باشد آن را به شیء Blob تبدیل میکنیم که میتوان کاری روی آن انجام داد. نتیجه این وضعیت به صورت زیر بازگشت مییابد:
1response => response.blob()
که اختصاری برای کد زیر است:
1function(response) {
2
3return response.blob();
4
5}
تا به اینجا توضیح کافی است. در ادامه کد زیر را در خط اول کد جاوا اسکریپت اضافه کنید:
1let promise2 = promise.then(response => response.blob());
هر فراخوانی به ()then. ایجاد یک Promise جدید را تضمین میکند. این وضعیت بسیار دقیق است زیرا متد Blob نیز یک Promise بازگشت میدهد و میتوانیم شیء Blob که بازگشت میدهد را با فراخوانی متد ()then. مربوط به Promise دوم به طور کامل اجرا کنیم. از آنجا که میخواهیم کار کمی پیچیدهتری نسبت به اجرای یک متد منفرد روی blob اجرا کنیم و نتیجه را بازگشت دهیم باید بدنه تابع را این بار درون آکولاد قرار دهیم، چون در غیر این صورت با خطا مواجه خواهیم شد.
کد زیر را به انتهای کد موجود بیفزایید:
1let promise3 = promise2.then(myBlob => {
2
3})
اکنون بدنه تابع اجراکننده را پر میکنیم. خطوط کد زیر را درون آکولادها اضافه کنید:
1let objectURL = URL.createObjectURL(myBlob);
2let image = document.createElement('img');
3image.src = objectURL;
4document.body.appendChild(image);
ما در اینجا مشغول اجرای متد ()URL.createObjectURL هستیم و آن را در زمان تکمیل شدن اجرای Promise دوم به صورت یک پارامتر Blob بازگشتی ارسال میکنیم. بدین ترتیب یک URL بازگشت مییابد که به شیء اشاره میکند. در ادامه یک عنصر <img> ایجاد میکنیم و خصوصیت src آن را برابر با URL شیء قرار میدهیم و آن را به DOM الحاق میکنیم تا تصویر روی صفحه نمایش پیدا کند.
اگر فایل HTML را که هم اینک ایجاد کردیم، ذخیره کنید و آن را در مرورگر بارگذاری نمایید، خواهید دید که تصویر مطابق انتظار در صفحه نمایش پیدا میکند.
نکته: احتمالاً متوجه شدهاید که این مثالها تا حدودی ساختگی هستند. ما این کار را میتوانستیم با یک عنصر <img> و تعیین خصوصیت src برابر با URL شیء رسانهای نیز انجام دهیم و نیازی به این همه زنجیره ()fetch و ()blob نبود. با این حال این مثال را انتخاب کردیم، زیرا Promise-ها را به روش سادهای معرفی میکند و دلیل آن مناسب بودن این رویکرد در کارکردهای واقعی نبوده است.
پاسخ به شکست
در بخش قبل یک مورد را فراموش کردیم اشاره کنیم. در کد فوق هیچ روشی برای مدیریت خطا در زمان شکست خوردن هر یک از promise-ها تعبیه نشده است. این شکست به زبان Promise «رد شدن» (Reject) نامیده میشود. در این حالت میتوان رویههای مدیریت خطا را با اجرای متد ()catch. روی Promise قبلی اضافه کرد. کد زیر را اضافه کنید:
1let errorCase = promise3.catch(e => {
2 console.log('There has been a problem with your fetch operation: ' + e.message);
3});
برای این که عملکرد این کد را ببینید، یک URL نادرست برای تصویر وارد کنید و تصویر را مجدداً بارگذاری نمایید. خواهید دید که خطا در کنسول ابزارهای توسعهدهنده مرورگر نمایش پیدا میکند.
البته اینک اتفاق بیشتری نسبت به زمانی که بلوک ()catch. کلاً وجود نداشت نمیافتد، اما اگر اندکی تأمل کنید متوجه میشوید که با افزودن این بلوک مدیریت خطا میتوان آن را دقیقاً به روش دلخواه مدیریت کرد. در یک اپلیکیشن واقعی بلوک ()catch. میتواند اقدام به واکشی مجدد تصویر کند یا تصویر پیشفرض را نمایش دهد یا از کاربر بخواهد که URL تصویر دیگری را ارائه کند.
زنجیره کردن بلوکها به همدیگر
روشی که تا به اینجا برای نوشتن کد استفاده کردیم یک روش کاملاً طولانی و دلیل این کار کمک به درک مطلب بوده است. چنان که قبلاً گفتیم میتوان بلوکهای ()catch. را به هم زنجیر کرد. بدین ترتیب کد فوق را میتوان به صورت زیر نیز نوشت:
1fetch('coffee.jpg')
2.then(response => response.blob())
3.then(myBlob => {
4 let objectURL = URL.createObjectURL(myBlob);
5 let image = document.createElement('img');
6 image.src = objectURL;
7 document.body.appendChild(image);
8})
9.catch(e => {
10 console.log('There has been a problem with your fetch operation: ' + e.message);
11});
به خاطر داشته باشید که مقدار بازگشتی از سوی یک Promise موفق به پارامتر ارسالی به بلوک ()then. بعدی تابع اجراکننده تبدیل میشود.
نکته: بلوکهای ()then()/.catch. در Promise-ها اساساً معادل ناهمگام بلوک try...catch در کد همگام هستند. به یاد بسپارید که try...catch همگام در کد ناهمگام عمل نمیکند.
جمعبندی اصطلاحات Promise
در بخش قبل در مورد مباحث زیادی صحبت کردیم، بنابراین تلاش میکنیم آن را در یک بخش کوتاه جمعبندی کنیم تا بتوانید در آینده در هنگام یادآوری این موضوع راحتتر مطالب را به خاطر بیاورید. همچنین میتوانید چندین بار بخش فوق را مطالعه کنید تا به خوبی در ذهنتان جا بیفتد.
- زمانی که یک Promise ایجاد میشود، نه در حالت موفقیت است و نه شکست، بلکه در حالت «انتظار» (Pending) است.
- زمانی که Promise بازگشت مییابد گفته میشود که resolve شده است.
- یک Promise موفق را Fulfilled مینامیم. این Promise یک مقدار بازگشت میدهد که میتوان در بلوک ()then. در انتهای زنجیره Promise به آن دسترسی داشت. تابع اجراکننده درون بلوک ()then. مقدار بازگشتی را در اختیار دارد.
- Promise ناموفق به نام rejected شناخته میشود. این Promise یک reason بازگشت میدهد که پیام خطایی است که نشان میدهد چرا Promise ناموفق بوده است. با مراجعه به بلوک ()catch. در انتهای زنجیره Promise میتوان به این دلیل دسترسی یافت.
اجرای کد در پاسخ به چند Promise موفق
مثال فوق برخی از مبانی واقعی استفاده از Promise-ها را به ما نشان داد. اکنون به بررسی برخی قابلیتهای پیشرفتهتر میپردازیم. در آغاز باید گفت که زنجیرهسازی پردازشها برای رخ دادن یکی پس از دیگری ممکن است، اما اگر بخواهیم کد خاصی فقط پس از آن که همه Promise ها موفق بودند، اجرا شود چطور؟
این کار با استفاده از متد استاتیکی به نام ()Promise.all میسر خواهد بود. این متد یک آرایه از Promise-ها به عنوان پارامتر ورودی میگیرد و یک شیء Promise را تنها در صورتی بازگشت میدهد که همه Promise-ها در آرایه موفق باشند. ساختار آن مانند زیر است:
1Promise.all([a, b, c]).then(values => {
2 ...
3});
اگر همه Promise-ها موفق شوند، بلوک ()then. تابع اجراکننده یک آرایهی شامل همهی نتایج را به عنوان پارامتر میگیرد. اگر هر کدام از Promise-های ارسالی به شیء ()Promise.All رد شوند، کل بلوک رد خواهد شد.
این متد میتواند کاملاً مفید باشد، تصور کنید که میخواهیم اطلاعاتی را به صورت دینامیک واکشی کنیم تا یک قابلیت UI را روی صفحه خود با محتوایی پر کنیم. در اغلب موارد بهتر است همه دادهها را دریافت کنیم و تنها محتوای کامل را نمایش دهیم تا این که بخواهیم اطلاعات ناقصی را به نمایش بگذاریم. در ادامه این مثال را عملاً میسازیم:
کد زیر را روی سیستم خود در یک فایل به نام index.html قرار دهید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>My test page</title>
6 </head>
7 <body>
8 <p>This is my page</p>
9 </body>
10</html>
در این مورد نیز عنصر <script> را درست پیش از تگ پایانی <body/> قرار دهید.
فایلهای تصویر coffee.jpg ،tea.jpg و فایل متنی description.txt را دانلود کنید. همچنین میتوانید فایلهای خود را جایگزین کنید.
ما در اسکریپت خود ابتدا بک تابع تعریف میکنیم که Promise-هایی را که قرار است به ()Promise.all ارسال شوند، بازگشت میدهد، بدین ترتیب اگر بخواهیم بلوک ()Promise.all را در پاسخ به پایان یافتن عملیات ()fetch اجرا کنیم سادهتر خواهد بود. روش کار به صورت زیر است:
1let a = fetch(url1);
2let b = fetch(url2);
3let c = fetch(url3);
4
5Promise.all([a, b, c]).then(values => {
6 ...
7});
زمانی که Promise کامل شد، values ارسالی به دستگیره تکمیل، میتواند شامل سه شیء Response باشد که برای هر کدام به یک عملیات تکمیل شده تعلق دارد.
با این حال ما نمیخواهیم این کار را انجام دهیم. برای کد ما مهم نیست که هر عملیات ()fetch چه زمانی انجام یافته است. بلکه میخواهیم دادهها را بارگذاری کنیم. این بدان معنی است که ما میخواهیم بلوک ()Promise.all را زمانی که دادهها در blob-های قابل استفاده بازگشت یافتند آنها را به صورت تصاویر و متن نمایش دهیم. میتوان تابعی نوشت که این کار را انجام دهد. کد زیر را درون عنصر <script> اضافه کنید:
1function fetchAndDecode(url, type) {
2 return fetch(url).then(response => {
3 if (type === 'blob') {
4 return response.blob();
5 } else if (type === 'text') {
6 return response.text();
7 }
8 })
9 .catch(e => {
10 console.log('There has been a problem with your fetch operation: ' + e.message);
11 });
12}
گرچه ممکن است کمی پیچیده به نظر برسد، اما آن را گام به گام بررسی میکنیم:
قبل از هر چیز تابعی تعریف کنید و یک URL و یک رشته به آن ارسال کنید که نشاندهنده نوع منبعی باشد که قرار است واکشی شود.
درون بدنه تابع، ساختار مشابهی داریم که در مثال نخست دیدیم. ما تابع ()fetch را برای واکشی منبعی در URL ذکر شده استفاده میکنیم و سپس آن را به Promise دیگری زنجیرهسازی میکنیم که بدنه پاسخ دیکُد شده را بازگشت میدهد. این مقدار بازگشتی همواره متد ()blob در مثال قبلی است.
با این حال، دو چیز در اینجا متفاوت هستند:
قبل از هر چیز، Promise دوم که بازگشت میدهیم بسته به آن مقدار type که میخواهیم متفاوت است. درون تابع اجراکننده یک گزاره if ... else if وجود دارد که بسته به نوع فایلی که قرار است دیکد شود، Promse متفاوتی بازگشت میدهد. در این مورد میتوانیم بین blob و text انتخاب کنیم، اما میتوان از انواع دیگری نیز استفاده کرد.
تفاوت دوم این است که یک کلیدواژه return قبل از فراخوانی ()fetch اضافه کردهایم. تأثیر آن این است که کل زنجیره اجرا میشود و سپس نتیجه نهایی، زمانی که مقدار بازگشتی تابع تعریفشده به دست آید، اجرا خواهد شد. در واقع، گزاره return نتیجه را به زنجیره فوقانی بازگشت میدهد.
در انتهای بلوک یک فراخوانی ()catch. را نیز زنجیره کردهایم که همه حالتهای خطا را که ممکن است در هر Promise ارسالی به آرایه ()all. وجود داشته باشند، مدیریت میکند. اگر هر کدام از Promise-ها رد شوند، بلوک catch به شما اطلاع خواهد داد که کدام بلوک مشکل دارد. بلوک ()all. همچنان موفق است، اما منابعی را که مشکل دارند نمایش نمیدهد. اگر بخواهید all. رد شود باید بلوک ()catch. را به انتهای آن زنجیره کنید.
کد درون بدنه تابع ناهمگام و مبتنی بر Promise است از این رو در عمل کل تابع به صورت یک Promise عمل میکند.
در ادامه تابع خود را سه بار فراخوانی میکنیم تا شروع به واکشی و دیکد کردن تصاویر و متن بکند و هر کدام از Promise-های بازگشتی را در یک متغیر ذخیره کند. کد زیر را به انتهای کد قبلی اضافه کنید:
1let coffee = fetchAndDecode('coffee.jpg', 'blob');
2let tea = fetchAndDecode('tea.jpg', 'blob');
3let description = fetchAndDecode('description.txt', 'text');
سپس یک بلوک ()Promise.all تعریف میکنیم تا برخی کدها تنها زمانی که هر سه Promise ذخیره شده فوق با موفقیت اجرا شدند، به اجرا درآیند. در آغاز یک بلوک با اجراکننده خالی درون فراخوانی ()then. به صورت زیر اضافه کنید:
1Promise.all([coffee, tea, description]).then(values => {
2
3});
چنان که میبینید این کد یک آرایه شامل Promise-ها به عنوان ورودی میگیرد. اجراکننده تنها زمانی اجرا خواهد شد که هر سه Promise بازگشت یابند. زمانی که این اتفاق بیفتد یک آرایه شامل نتایج از Promise-های منفرد بازگشت مییابد که تا حدودی شبیه به آرایه زیر است:
[coffee-results، tea-results، description-results]
در نهایت کد زیر را درون اجراکننده اضافه کنید. در این کد ما از کد همگام نسبتاً سادهای برای ذخیرهسازی متغیرها در متغیرهای جداگانه استفاده کردیم و سپس تصاویر و متن را روی صفحه نمایش میدهیم:
1console.log(values);
2// Store each value returned from the promises in separate variables; create object URLs from the blobs
3let objectURL1 = URL.createObjectURL(values[0]);
4let objectURL2 = URL.createObjectURL(values[1]);
5let descText = values[2];
6
7// Display the images in <img> elements
8let image1 = document.createElement('img');
9let image2 = document.createElement('img');
10image1.src = objectURL1;
11image2.src = objectURL2;
12document.body.appendChild(image1);
13document.body.appendChild(image2);
14
15// Display the text in a paragraph
16let para = document.createElement('p');
17para.textContent = descText;
18document.body.appendChild(para);
صفحه را ذخیره و رفرش کنید تا کامپوننتهای UI بارگذاری شده را مشاهده کنید، البته شاید ظاهر آنها چندان جذاب نباشد. کدی که در اینجا برای نمایش آیتمها ارائه شده است کاملاً ابتدایی است، اما فعلاً به عنوان مثال توضیحی کار میکند. اگر در هر مرحله از این کدنویسی مشکل داشتید میتوانید از کد کامل شده زیر استفاده کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>fetch() promise.all() example</title>
6 </head>
7 <body>
8 <script>
9 // Define function to fetch a file and return it in a usable form
10 function fetchAndDecode(url, type) {
11 // Returning the top level promise, so the result of the entire chain is returned out of the function
12 return fetch(url).then(response => {
13 // Depending on what type of file is being fetched, use the relevant function to decode its contents
14 if(type === 'blob') {
15 return response.blob();
16 } else if(type === 'text') {
17 return response.text();
18 }
19 })
20 .catch(e => {
21 console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
22 });
23 }
24 // Call the fetchAndDecode() method to fetch the images and the text, and store their promises in variables
25 let coffee = fetchAndDecode('coffee.jpg', 'blob');
26 let tea = fetchAndDecode('tea.jpg', 'blob');
27 let description = fetchAndDecode('description.txt', 'text');
28 // Use Promise.all() to run code only when all three function calls have resolved
29 Promise.all([coffee, tea, description]).then(values => {
30 console.log(values);
31 // Store each value returned from the promises in separate variables; create object URLs from the blobs
32 let objectURL1 = URL.createObjectURL(values[0]);
33 let objectURL2 = URL.createObjectURL(values[1]);
34 let descText = values[2];
35 // Display the images in <img> elements
36 let image1 = document.createElement('img');
37 let image2 = document.createElement('img');
38 image1.src = objectURL1;
39 image2.src = objectURL2;
40 document.body.appendChild(image1);
41 document.body.appendChild(image2);
42 // Display the text in a paragraph
43 let para = document.createElement('p');
44 para.textContent = descText;
45 document.body.appendChild(para);
46 });
47 </script>
48 </body>
49</html>
نکته: اگر میخواهید کد فوق را بهبود بدهید میتوانید یک حلقه روی لیستی از آیتمهایی که قرار است نمایش یابند تعریف کنید و هر کدام را واکشی و دیکُد کنید. در ادامه روی نتایج درون ()Promise.all حلقهای تعریف کنید و تابع متفاوتی را برای نمایش هر یک بسته به نوع کد مورد استفاده قرار دهید. بدین ترتیب میتوانید کد فوق را برای هر تعداد از آیتمها و هر نوع از آنها استفاده کنید.
علاوه بر آن میتوانید نوع فایل واکشی شده را نیز بدون نیاز به وجود صریح مشخصه type تعیین کنید. برای نمونه میتوانید هدر HTTP با عنوان Content-Type را در مورد هر پاسخ با استفاده از کد زیر بررسی کنید:
1response.headers.get("content-type")
بدین ترتیب میتوانید بر اساس نوع هر فایل واکنش متناسبی داشته باشید.
اجرای کد نهایی پس از موفقیت/شکست Promise
مواردی وجود دارند که ممکن است بخواهید یک بلوک نهایی کد پس از تکمیل شدن Promise اجرا شود و مهم نیست که Promise موفق یا ناموفق بوده است. قبلاً دیدیم که میتوان کد یکسانی را در Callback-های ()then. و ()catch. برای مثال به صورت زیر قرار دارد:
1myPromise
2.then(response => {
3 doSomething(response);
4 runFinalCode();
5})
6.catch(e => {
7 returnError(e);
8 runFinalCode();
9});
در مرورگرهای جدیدتر متد ()finally. نیز وجود دارد که میتوان به انتهای زنجیرهی Promise معمول زنجیرهسازی کرد و امکان جلوگیری از تکرار کردن کد و اجرای منسجمتر کارها را فراهم میسازد. اکنون کد فوق میتواند به صورت زیر نوشته شود:
1myPromise
2.then(response => {
3 doSomething(response);
4})
5.catch(e => {
6 returnError(e);
7})
8.finally(() => {
9 runFinalCode();
10});
برای مثال عملی نگاهی به کد زیر بیندازید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>fetch() promise.finally() example</title>
6 </head>
7 <body>
8 <script>
9 // Define function to fetch a file and return it in a usable form
10 function fetchAndDecode(url, type) {
11 // Returning the top level promise, so the result of the entire chain is returned out of the function
12 return fetch(url).then(response => {
13 // Depending on what type of file is being fetched, use the relevant function to decode its contents
14 if(type === 'blob') {
15 return response.blob();
16 } else if(type === 'text') {
17 return response.text();
18 }
19 })
20 .catch(e => {
21 console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
22 })
23 .finally(() => {
24 console.log(`fetch attempt for "${url}" finished.`);
25 });
26 }
27 // Call the fetchAndDecode() method to fetch the images and the text, and store their promises in variables
28 let coffee = fetchAndDecode('coffee.jpg', 'blob');
29 let tea = fetchAndDecode('tea.jpg', 'blob');
30 let description = fetchAndDecode('description.txt', 'text');
31 // Use Promise.all() to run code only when all three function calls have resolved
32 Promise.all([coffee, tea, description]).then(values => {
33 console.log(values);
34 // Store each value returned from the promises in separate variables; create object URLs from the blobs
35 let objectURL1 = URL.createObjectURL(values[0]);
36 let objectURL2 = URL.createObjectURL(values[1]);
37 let descText = values[2];
38 // Display the images in <img> elements
39 let image1 = document.createElement('img');
40 let image2 = document.createElement('img');
41 image1.src = objectURL1;
42 image2.src = objectURL2;
43 document.body.appendChild(image1);
44 document.body.appendChild(image2);
45 // Display the text in a paragraph
46 let para = document.createElement('p');
47 para.textContent = descText;
48 document.body.appendChild(para);
49 });
50 </script>
51 </body>
52</html>
کد فوق دقیقاً همانند دموی ()Promise.all کار میکند که در بخش قبل دیدیم به جز این که در تابع ()fetchAndDecode یک متد ()finally زنجیرهسازی کردیم که به انتهای آن اضافه میشود:
1function fetchAndDecode(url, type) {
2 return fetch(url).then(response => {
3 if(type === 'blob') {
4 return response.blob();
5 } else if(type === 'text') {
6 return response.text();
7 }
8 })
9 .catch(e => {
10 console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
11 })
12 .finally(() => {
13 console.log(`fetch attempt for "${url}" finished.`);
14 });
15}
بدین ترتیب پیامهایی در کنسول لاگ میشود که اعلام میکند چه زمانی fetch به پایان رسیده است.
نکته: ()finally امکان نوشتن معادلهای ناهمگام برای try/catch/finally را در کد همگام فراهم میسازد.
ساخت Promise-های سفارشی
خبر خوب این است که ما قبلاً به ترتیبی Promise سفارشی خود را ساختهایم. زمانی که چندین Promise را با استفاده از بلوکهای ()then. به هم زنجیر کنیم، یا این که آنها را با ایجاد کارکرد سفارشی با هم ترکیب کنیم، در واقع تابع مبتنی بر Promise سفارشی ناهمگام خاص خود را ساختهایم. برای نمونه تابع ()fetchAndDecode را در مثال قبلی در نظر بگیرید.
ترکیب کردن API-های مبتنی بر Promise دیگر برای ایجاد کارکرد سفارشی با فاصله زیادی، رایجترین روش برای انجام کارهای سفارشی با Promise-ها است و انعطافپذیری و قدرت مدرنترین API-ها را پیرامون همین مفهوم نمایش میدهد. با این حال روش دیگری نیز برای ساخت Promise سفارشی وجود دارد.
استفاده از سازنده ()Promise
شما میتوانید Promise-های خاص خود را با استفاده از سازنده ()Promise بسازید. راهحل اصلی که این وضعیت به کارمی آید زمانی است که یک کد API مبتنی بر سبک کدهای ناهمگام قدیمی و غیر مبتنی بر Promise دارید و میخواهید آن را با Promise بازنویسی کنید. این وضعیت زمانی بسیار کارآمد است که لازم باشد از کد پروژهها، کتابخانهها یا فریمورکهای قدیمی موجود به همراه کدهای مبتنی بر Promise جدید استفاده کنید.
در ادامه مثال سادهای را میبینید که در آن یک فراخوانی ()setTimeout درون یک Promise قرار گرفته است. بدین ترتیب دو تابع اجرا میشوند که Promise را با استفاده از عبارت «Success!» نهایی و یا در اصطلاح resolve میکنند.
1let timeoutPromise = new Promise((resolve, reject) => {
2 setTimeout(function(){
3 resolve('Success!');
4 }, 2000);
5});
()resolve و ()reject دو تابعی هستند که برای موفقیت یا شکست Promise اخیراً ایجاد شده مورد استفاده قرار میگیرند. در این حالت، Promise با عبارت «!Success» به صورت fulfilled درمیآید.
بنابراین زمانی که این Promise را فراخوانی میکنید میتوانید یک بلوک ()then. را به انتهای آن زنجیر کنید و بدین ترتیب یک رشته به صورت «!Success» ارسال میکند. در کد زیر یک پیام را به صورت هشدار ارائه میکنیم:
1timeoutPromise
2.then((message) => {
3 alert(message);
4})
یا این که صرفاً میتوانیم بنویسیم:
1timeoutPromise.then(alert);
کد منبع کامل این مثال به صورت زیر است:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Custom promise example</title>
6 </head>
7 <body>
8 <script>
9 // Define custom promise function
10 let timeoutPromise = new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Success!');
13 }, 2000);
14 });
15 timeoutPromise
16 .then(message => {
17 alert(message);
18 })
19 </script>
20 </body>
21</html>
مثال فوق چندان انعطافپذیر نیست. Promise میتواند صرفاً با یک رشته fulfill شود و هیچ نوع شرایط ()reject نداشته باشد. بدیهی است که متد ()setTimeout هیچ شرایط شکستی ندارد و از این رو این مسئله در این مثال موضوعیت ندارد.
رد کردن یک Promise سفارشی
میتوانیم یک Promise سفارشی بسازیم که درست مانند ()resolve با استفاده از متد ()reject ریجکت شود. این متد یک مقدار منفرد میگیرد، اما در این حالت این همان دلیل ریجکت شدن، یعنی خطایی است که به بلوک ()catch. ارسال خواهد شد.
مثال قبلی را با نوعی شرایط ()reject بسط میدهیم و همچنین اجازه میدهیم پیامهای مختلفی به محض موفقیت ارسال شوند.
یک کپی از کد زیر روی سیستم خود بسازید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Custom promise example</title>
6 </head>
7 <body>
8 <script>
9 // Define custom promise function
10 let timeoutPromise = new Promise((resolve, reject) => {
11 setTimeout(() => {
12 resolve('Success!');
13 }, 2000);
14 });
15 timeoutPromise
16 .then(message => {
17 alert(message);
18 })
19 </script>
20 </body>
21</html>
و تعریف ()timeoutPromise موجود را با کد زیر عوض کنید:
1function timeoutPromise(message, interval) {
2 return new Promise((resolve, reject) => {
3 if (message === '' || typeof message !== 'string') {
4 reject('Message is empty or not a string');
5 } else if (interval < 0 || typeof interval !== 'number') {
6 reject('Interval is negative or not a number');
7 } else {
8 setTimeout(function(){
9 resolve(message);
10 }, interval);
11 }
12 });
13};
در کد فوق دو آرگومان به تابع سفارشی خود ارسال میکنیم که یکی پیامی برای انجام یک کار و دیگری بازه زمانی است که باید پیش از انجام آن کار منتظر بماند. درون تابع یک شیء Promise جدید بازگشت میدهیم که تابعی را فرامیخواند که Promise مورد نظر ما را بازگشت میدهد.
- درون سازنده Promise چند بررسی درون سازه if…else اجرا میکنیم.
- قبل از هر چیز بررسی میکنیم که آیا پیام برای هشدار دادن مناسب است یا نه. اگر یک رشته خالی باشد یا اولاً رشته نباشد Promise را با پیام خطای مناسبی ریجکت میکنیم.
- سپس بررسی میکنیم که آیا بازه مقدار مناسبی دارد یا نه. اگر منفی باشد یا عدد نباشد، Promise را با پیام خطای مناسبی ریجکت میکنیم.
- در نهایت اگر پارامترها هر دو OK به نظر برسند، Promise پس از بازه معین شده با استفاده از ()setTimeout با پیام خاصی ریجکت میکند.
- از آنجا که تابع ()timeoutPromise یک Promise بازگشت میدهد، میتوانیم ()then() ،.catch. و غیره را با هم ترکیب کنیم. اکنون از آن استفاده میکنیم، کاربرد timeoutPromise قبلی را با کد زیر عوض میکنیم:
1timeoutPromise('Hello there!', 1000)
2.then(message => {
3 alert(message);
4})
5.catch(e => {
6 console.log('Error: ' + e);
7});
زمانی که کد را ذخیره و اجرا کنید، پس از یک ثانیه، پیام هشدار را دریافت خواهید کرد. برای نمونه اکنون تلاش میکنیم پیام را به یک رشته خالی یا بازه را به شماره منفی تنظیم میکنیم و میتوانید ببینید که Promise با پیامهای خطای مناسبی ریجکت میشوند. همچنین میتوانید چیز دیگری را نیز با پیامهای resolve شده امتحان کنید,
نکته: میتوانید نسخه کامل این مثال را در ادامه مشاهده کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Advanced custom promise example</title>
6 </head>
7 <body>
8 <script>
9 // Define custom promise function
10 function timeoutPromise(message, interval) {
11 return new Promise((resolve, reject) => {
12 if(message === '' || typeof message !== 'string') {
13 reject('Message is empty or not a string');
14 } else if(interval < 0 || typeof interval !== 'number') {
15 reject('Interval is negative or not a number');
16 } else {
17 setTimeout(function(){
18 resolve(message);
19 }, interval);
20 }
21 });
22 };
23 timeoutPromise('Hello there!', 1000)
24 .then(message => {
25 alert(message);
26 })
27 .catch(e => {
28 console.log('Error: ' + e);
29 });
30 </script>
31 </body>
32</html>
یک مثال واقعی
مثال فوق عامدانه ساده حفظ شده تا درک مفاهیم آسان بماند، اما در عمل کاملاً ناهمگام نیست. ماهیت ناهمگام اساساً با استفاده از ()setTimeout جعل میشود، گرچه همچنان نشان میدهد که Promise-ها برای ایجاد تابع سفارشی با گردش کار معینی از عملیات، مدیریت مناسب خطا و موارد دیگر مفید هستند.
یک مثال که شما را دعوت میکنیم تا مطالعه کنید کتابخانه idb مربوط به Jake Archibald است که اپلیکیشن ناهمگام مفیدی برای سازنده ()Promise نمایش میدهد. این کتابخانه از API مربوط به IndexedDB استفاده میکند که یک API مبتنی بر Callback به سبک قدیمی است که برای ذخیرهسازی و بازیابی دادهها در سمت کلاینت استفاده میشود و امکان بهرهگیری از آن به همراه Promise را میدهد. اگر به فایل کتابخانه اصلی نگاه کنید، میبینید که از همان نوع تکنیکی که در این نوشته معرفی کردیم استفاده شده است. در بلوک کد زیر یک مدل درخواست مقدماتی که از سوی تعداد زیادی از متدهای IndexedDB استفاده میشود برای بهرهگیری از Promise تبدیل یافته است:
1function promisifyRequest(request) {
2 return new Promise(function(resolve, reject) {
3 request.onsuccess = function() {
4 resolve(request.result);
5 };
6
7 request.onerror = function() {
8 reject(request.error);
9 };
10 });
11}
این وضعیت به وسیله افزودن چند دستگیره رویداد عمل میکند که Promise را در زمانهای مناسب fulfill یا Reject میکنند:
- زمانی که رویداد موفقیت درخواست تحریک شود، دستگیره onsuccess اقدام به fulfill کردن Promise با نتیجه (result) درخواست میکند.
- زمانی که رویداد خطای درخواست تحریک شود، اشیای دستگیره onerror اقدام به ریجکت کردن Promise با شیء error درخواست میکند.
سخن پایانی
Promise-ها روش مناسبی برای ساخت اپلیکیشنهای ناهمگام هستند که وقتی مقدار بازگشتی از تابع یا میزان مدتی که بازگشت آن طول میکشد را ندانیم به کار میآیند. بدین ترتیب بیان و استدلال در مورد توالی عملیات ناهمگام بدون Callback-های عمیقاً تو در تو آسانتر میشود و از استایل مدیریت خطایی پشتیبانی میکنند که مشابه گزاره try...catch ناهمگام است.
Promise-ها در جدیدترین نسخهی همه مرورگرهای مدرن استفاده میشوند. تنها مکانی که پشتیبانی از Promise مشکل محسوب میشود، مرورگرهای Opera Mini و IE11 و نسخههای قبلتر آن است.
ما در این مقاله همه قابلیتهای Ptomise-ها را بررسی نکردیم، بلکه صرفاً انواع مفید و جالبتر را مورد بررسی قرار دادیم. زمانی که شروع به یادگیری Promise-ها بکنید، با قابلیتها و تکنیکهای بیشتری مواجه خواهید شد.
اغلب API-های مدرن وب مبتنی بر Promise هستند، از این رو باید آنها را به خوبی یاد بگیرید تا بتوانید بیشترین بهرهبرداری را از آنها داشته باشید. از جمله این API-ها WebRTC ،Web Audio API ،Media Capture and Streams و موارد دیگر هستند. Promise-ها به مرور زمان اهمیت بیشتری کسب میکنند، بنابراین یادگیری استفاده از آنها گام مهمی در یادگیری جاوا اسکریپت مدرن محسوب میشود. برای مطالعه بخش بعدی به لینک زیر رجوع کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی جاوا اسکریپت
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- Promise در جاوا اسکریپت و کاربردهای آن — به زبان ساده
==