Sam Aghapour
Sam Aghapour
خواندن ۱۴ دقیقه·۳ سال پیش

پشت پرده ی جاوااسکریپت! Promise ها چگونه کار میکنند؟

promises in javascript
promises in javascript

سلام دوستان?

وقتی یه زبانی رو یاد میگیرم دوس دارم برای مسلط شدن بهش بفهمم فلان مبحث در پشت پرده ی این زبان چجوری هندل میشه تا بتونم موقع استفاده ازش درک بهتری داشته باشم و در نتیجه باگ کمتری تولید کنم.

اگه شما هم جزء اون دسته برنامه نویسایی هستید که دل و روده مباحث رو تا نریزن بیرون آروم نمیگیگیرن بیاید شروع کنیم?

نکته: اگه درمورد event loop , callback queue , call stack و کلا نحوه هندل شدن توابع async در جاوااسکریپت که پیش نیاز مقاله فعلی هستند، چیزی نمیدونید، لطفا لطفا لطفا قبل شروع این مقاله ، این موارد رو یاد بگیرید که یه مقاله هم راجبشون نوشتم میتونید با کلیک روی این لینک اول اون رو مطالعه کنید و بعد به این مقاله وارد بشید.

یکی از این مباحث مهم که میخوایم دل و رودش رو بریزیم، promise ها در جاوااسکریپته. حالا چیزایی که ما قراره توی این مقاله به عنوان موضوع اصلی و مکمل هاش یاد بگیریم:

  1. همه چیز راجب Promise ها و نحوه هندل شدن و چیستی و چرایی و کلی مثال از دنیای واقعی و کد و...
  2. فانکشن هایی با عنوان callback و موضوعی به اسم callback hell
  3. چگونگی به وجود اومدن async در جاوااسکریپت
  4. مبحث فوق العاده ای با عنوان microtask queue و macrotask queue
  5. سینتکس async / await
  6. دیباگ کردن با بلاک های try / catch / finally
  7. ریکوئست زدن با Promise.all و Promise.allSettled


قول یا Promise در جاوااسکریپت

بزارید این مبحث رو با یه مثال از دنیای واقعی شروع کنیم:

پسری رو در نظر بگیرید که دو هفته دیگه تولدش هست و مادرش بهش قول میده که تا دوهفته دیگه یه کیک براش میپزه و توی این دو هفته که مادره مشغول انجام کارای کیک هست پسره مثل شلغم نمیشینه منتظر بشینه تا مادره کیک رو بپزه و بعد بره مابقی کارای تولد رو انجام بده! اینطوری همه چی بلاک و معلق میشه بخاطر یه دونه کیک :/ ، بلکه پسره طی این زمان به بقیه کاراش میرسه از جمله تهیه کردن بقیه چیزا واسه جشن تولد و... حالا این وضعیتِ قولی که مادر داده تا زمانی که تکلیفش مشخص نشده در حالت Pending یا انتظار قرار داره... وقتی تکلیفش مشخص شه اون حالت pending به یکی از دو حالت دیگه میتونه تغییر بکنه:

  1. حالت موفقیت آمیز یا fulfilled یا resolved
  2. حالت ردشده یا rejected

اگه فرض رو بر این بگیریم که مادره مریض میشه و نمیتونه کیک رو بپزه پسره قراره یه واکنشی نشون بده(ناراحت میشه) و اگه مادره اتفاق بدی نیفته و بتونه کیک رو بپزه پسره یه واکنش دیگه نشون میده (خوشحال میشه) و در نتیجه چه کیک پخته شه چه نشه یه چیز رو در نهایت هر حالتی اتفاق بیفته پسره انجام میده و اون برگزار کردن جشن هست.

عکس بالا مثالی که ما زدیم رو قشنگ نشون میده. وقتی پسره نمیشینه منتظر کیک بمونه و همزمان به بقیه کاراش میرسه به اون پروسه ای که کیک داره طی میکنه میگن asynchronous یا غیر همزمان. توی دنیای جاوااسکریپت هم اینطوری میشه گفت که این زبان به علت single thread بودن فقط میتونه یه خط کد رو در یک زمان انجام بده و نه دوتا پروسه رو همزمان!و یه زبان synchronous هست. پس پروسه های async یا غیر همزمان مثل ajax request ( گرفتن اطلاعات از سرور) در جاوااسکریپت چجوری هندل میشن؟ همون چیزی که موضوع امروز بحثمونه Promise ها ?

وقتی جاوااسکریپت به یه خط غیر همزمان مثل ajax request میخوره(fetch یا axios و...)، میدونه که این یه مقدار زمانی رو طول میکشه تا بره و دیتارو بگیره و بیاره پس بجاش یه آبجکت برمیگردونه که بهش promise یا قول گفته میشه، حالا بیاید چک کنیم این ابجکت شامل چه پراپرتی هایی هست:

promiseobject
promiseobject

توخط بالا یه ریکوست زدیم تا از سرور یه سری اطلاعات بگیریم و بریزیمش توی متغیر data اما تا این دیتا برسه یه مقدار طول بکشه پس جاوااسکریپت مثل شلغم نمیشینه بمونه تا این بره از سرور یه چیزی بگیره بیاد بعد مابقی کدهارو انجام بده چون توی این شرایط ابجکتی داره به اسم promise که اونو برمیگردونه و به بقیه کاراش میرسه. حالا این ابجکت شامل چیزایی هست که توی تصویر هم مشاهده میکنید:

  1. یک پراپرتی با اسم promiseState یا وضعیت که شامل یکی از سه حالاتی که بالاتر هم گفتیم میتونه باشه: 1.pending یا انتظار که یعنی هنوز در حال گرفتنه و تکلیف مشخص نیست. 2.fulfilled یا موفقیت آمیز که یعنی دیتارو با موفقیت گرفته 3.rejacted یا ردشده که یعنی دیتارو نتوسنته بگیره و با یه خطایی مواجه شده
  2. یک پراپرتی با اسم promiseResult یا نتیجه داریم که مقدارش وابسته به پراپرتی promiseState تغییر میکنه. نمونه در عکسهای زیر:
وقتی وضعیت pending باشه نتیجه برابر با undefined هست
وقتی وضعیت pending باشه نتیجه برابر با undefined هست
وقتی وضعیت rejected باشه نتیجه برابر با پیغامِ اروری هست که میده
وقتی وضعیت rejected باشه نتیجه برابر با پیغامِ اروری هست که میده
وقتی وضعیت fulfilled باشه نتیجه برابر با چیزی هست که از سمت سرور اومده
وقتی وضعیت fulfilled باشه نتیجه برابر با چیزی هست که از سمت سرور اومده


3.ابجکت پروتوتایپی که اگه مقاله پروتوتایپ رو خونده باشید میدونید که این یه ابجکتیه که ایشون به ارث بردن و داخلش سه تا متد هست که وابسته به پراپرتی promiseState یا وضعیت، به طور اتوماتیک اجرا میشن که یکی یکی بررسی میکنیم:

  1. متد catch که یک فانکشن با یک پارامتر(که مقدارش همون مقدارِ پراپرتی promiseResult هست) میگیره و فقط و فقط زمانی که وضعیت rejacted باشه اون فانکشن رو اجرا میکنه و اگه وضعیت جور دیگه ای باشه این اصلا اجرا نمیشه
  2. متد then که یک فانکشن با یک پارامتر(که مقدارش همون مقدارِ پراپرتی promiseResult هست) میگیره و فقط و فقط زمانی که وضعیت resolved باشه اون فانکشن رو اجرا میکنه و اگه وضعیت جور دیگه ای باشه این اصلا اجرا نمیشه
  3. متد finally که با دوتای بالا فرق داره و وضعیت چه rejacted باشه چه resolved ، بازم اجرا میشه

توی مثالی که در اول مقاله زدیم هم، واکنش ناراحت شدنِ پسر به قول مادرش مثل اجرا شدن فانکشن داخلِ متدِ catch هست ،واکنش خوشحال شدنِ پسر به قول مادرش مثل اجرا شدن فانکشن داخلِ متدِ then هست، و برگزار کردن تولد در هر دو صورت مثل اجرا شدن فانکشن داخلِ متدِ finally هست.

حالا یک مثال از این متد های catch , then , finally بزنیم برای وضعیت های مختلف:

توی مثال بالا اول وضعیت پرامیس pending هستش بعدش وضعیت rejected شد(حالا به هر دلیلی) و با rejected شدن وضعیت، فانکشن یا callback داخلِ catch اجرا شد و بعدش فانکشن داخلِ finally اجرا شد.

توی مثال بالا اول وضعیت پرامیس pending هستش بعدش وضعیت resolved شد و با resolved شدن وضعیت، فانکشن یا callback داخلِ then اجرا شد و بعدش فانکشن داخلِ finally اجرا شد.


مبحث callback ها در جاوااسکریپت + پیدایش async

توی مثالای بالا به callback اشاره شد که شاید برای بعضیاتون سوال پیش بیاد که چی هستش؟

دوستان به فانکشنی که به عنوان آرگومان پاس داده میشه به یه فانکشن دیگه و بعد از اتمام پروسه ی داخل اون فانکشن، فانکشن پارامتر اجرا میشه callback گفته میشه.

اگه یکم بخوام برگردم عقب، وقتی جاوااسکریپت نمیتونست کارای async انجام بده و مجبور بود بخاطر single thread بودن کلی بلاک شه و معطل بمونه تا یه پروسه ی نسبتا طولانی به اتمام برسه و بتونه خط های بعدی رو اجرا بکنه. بعدش برای حل مشکل بلاک شدن روی پروسه های طولانی مدت، اینترفیس یا رابطی به اسم asynchronous به وجود اومد که با گرفتن یه فانکشن به عنوان پارامتر که همون callback خودمونه، دیگه جاوااسکریپت مجبور نبود بلاک ومعطل بمونه و در عوض میگفت در حین اینکه این پروسه طولانی انجام میشه من میرم سره بقیه کدا ولی تو بیا این کال بک رو بگیر و وقتی این پروسه تموم شد فوراََ اجراش کن.

نکته: کال بک ها asynchronous نیستند و عملا sync هستند، فقط قابلیت این رو دارند که بتونیم باهاشون کدای async اجرا بکنیم و پروسه های async یا غیرهمزمانی مثل ajax request بعد از fulfilled شدن، کال بک خودشون رو صدا میکنن تا یه بلایی سر دیتایی که برگردوندن بکنن.

بیاید یه مثال کد از کال بک ها ببینیم:

در مثال بالا وقتی getData اجرا شده ajax request رو انجام داده و پارامتر دوم یه کال بک داده تا هروقت تکلیف دیتایی که داره از این ریکوئست برمیگرده مشخص شد این کال بک رو دیتا انجام بده و پارامتری که کال بکمون میگیره همون دیتا هستش.

مشکل callback hell

حالا مشکلی که این کال بک ها به وجود اوردن و باعث به وجود اومدن promise ها شدن اینه که وقتی بخوایم بلایی سر دیتا توی کال بک بیاریم که خودش async هست باید بازم مداخل اون کال بک ، کال بکِ دیگه ای رو انجام بدیم و اگه بازم داخل همون کال بک یه پروسه async دیگه بریم باز یه کال بکِ دیگه و.... زنجیره ای از کال بک ها که پشت هم پس از اون یکی اجرا میشن و این خیلی کد رو شلوغ و نامرتب میکنه که بهش callback hell گفته میشه.

توی نمونه بالا ما بعد از بدست اوردن دیتا اولین کال بک صدا زده میشه و بعد از اتمام اون کال بک، ریکوئست دوم زده میشه و کال بک بعدی انجام میشه و به ترتیب این کار چند بار یکی پشت دیگری انجام میشه و همونطور که تو عکس میبینید یه جهنم واقعی از کال بک هارو میبینید که در مقیاس های بزرگتر حتی خیلی بدتر میتونه باشه و اینکه گرفتن ارور و هندل کردنش هم توی این روش خیلی عجیب غریب و سخت تره و اینطوری شد که promise ها اومدن تا با then. و catch. کارمون رو راحتتر کنن.


مبحث Promises chaining

پرامیس ها اومدن تا کار مارو از لحاظ پیچیدگی کال بک های تو در تو راحت کنن اما مثل اینکه کافی نبود! وقتی بخوایم مثالی که توی callback hell زدیم رو با then. و catch. پیاده کنیم اگرچه خوانا تر میشه کدمون اما ما بازم دنبال روش راحتتری هستیم. بیاید یه مثال ببینیم:

Promises chaining
Promises chaining


توی مثال بالا میبینید که ما یه دیتایی رومیگیریم و یه بلایی سرش میاریم (در این مثال فقط لاگ میگیریمش) اما بعدش با یه ریکوئست دیگه یه پرامیس دیگه برمیگردونیم و بعدش یک .then دیگه میزاریم و دیتای این ریکوئست جدید رو هم یه کاریش میکنیم(در این مثال لاگ میگیریم) و به همین ترتیب چندین پرامیس پشت سر هم میفرستیم و با then ها یه بلایی سرشون میاریم که به این عمل، زنجیره پرامیس ها یا promises chaining گفته میشه.


پیدایش سینتکس async / await

برای حل کردن مشکل زنجیره ای شدن پرامیس ها، جاوااسکریپت در es8 سینتکس جدیدی که از همون پرامیس ها استفاده میکنه معرفی کرد تا با شبیه کردن بیشترِ کدهای async به sync دیگه خیلی خیلی خوانایی کد رو راحت تر کنه.

async / await
async / await

طبق مثال بالا سینتکس async / await به اینصورت هست که:

1. به فانکشنی که داخلش ریکوئست اتفاق میفته، کلمه async بهش اضافه بشه تا این فانکشن قابلیت async رو داشته باشه

2. به قبل ریکوئستی که میدیم کلمه await داده بشه تا جی اس عملیات داخل فانکشن رو (فقط داخل فانکشن رو) تا زمانی که تکلیف این ریکوست مشخص شه ، متوقف کنه

3. بعدش هر کاری که انجام بدیم دقیقا بعد از روشن شدن تکلیف اون پرامیس انجام میشه و دیگه نیازی به نوشتن .then و.. نیست.


نکته ی جالب توجه اینجاست که الان فانکشن getData، خودش یه پرامیس برمیگردونه چون یه فانکشن async هستش و پروسه داخلش به طور غیر همزمان داره هندل میشه و ما اگه بخوایم میتونیم بعد از صدا زدن این فانکشن بهش .then و... اضافه کنیم و کار خاصی رو انجام بدیم:

همونطور که میبینید ما با صدا زدن getData و اضافه کردن then بهش اول توی حالت پندینگ هست این فانکشن و بعدش برای هر ریکوست که داخل فانکشن زده شده عملیاتی که میخوایم رو انجام داده(در این مثال گرفتن هر دیتا و لاگ گرفتنش) و بعد از انجام موفقیت امیز همشون اومده حالا عملیاتی که داخل .then تعریف کردیم رو انجام داده ( که توی این مثال لاگ گرفتن جمله all requests are done هست ) اگه پروسه ی داخل فانکشن وضعیت rejected برمیگردوند بجای .then ، کال بک داخل .catch رو اجرا میکرد.


مبحث try / catch / finally

ما برای امتحان کردن کدامون میتونیم از بلاک هایی مثلا try و catch و finally استفاده کنیم:

توی مثال بالا ما ریکوئست هامون رو داخل بلاکِ try گذاشتیم و اگه مشکلی نباشه که به درستی اجرا و بدون رفتن تو بلاکِ catch به finally میره و بعد تموم میشه و اگه بلاک finally نباشه هم که بازم تمومه. حالا توی این مثال سه تای اول رو اجرا و وقتی توی jobs ارور میگیره میپره توی بلاک catch و بعد از اون بلاکِ finally.

هر خطی از کد که داخل بلاکِ try نوشتیم ارور داشته باشه دیگه جاوااسکریپت بقیه خط های داخل بلاک رو اجرا نمیکنه و یه راست میره کدی که داخل بلاک catch نوشتیم رو اجرا میکنه پارامتری که برابر با همون ارور هستش رو میگیره و میتونیم باهاش هرکاری کنیم (توی این مثال لاگ گرفتیم تا ببینیم ارور چیه) و در نهایت اگه بلاک finally وجود داشته باشه اجراش میکنه و در صورت وجود نداشتن به بقیه کاراش ادامه میده.

نکته :موقع استفاده از بلاکِ try ،استفاده از بلاک catch به همراهش ضروری هستش چون به هرحال ما داریم یه چیزی رو امتحان میکنیم تا اگه اروری داشت بگیریمش وقتی catch نباشه با چی بگیریمش؟
اما بلاکِ finally ضروری نیست و میتونیم بزاریمش یا نزاریمش.

این بلاک ها مارو توی دیباگ کردن کد ها خیلی کمک میکنن و مخصوصا موقع استفاده از async / await جای خالیِ catch. و then. رو برامون پر میکنن.



مبحث microtask queue و macrotask queue

توی مقاله ی "پشت پرده ی جاوا اسکریپت! توابع asynchronous چطور کار میکنند؟" ، اینو فهمیدیم که جاوااسکریپت تسک های sync رو میریزه تو call stack و کال بک ها میرن داخل web api و وقتی زمان اجراشون رسید داخل callback queue ریخته میشن، حالا این callback queue اسمهای دیگه ای از جمله macrotask queue و یا task queue هم داره که اینجا با macrotask queue بهش اشاره میکنم.

حالا یه چیز جدیدی که توی اون مقاله نگفتم و اینجا جای مناسبیه که بازش کنم microtask queue هستش،
ببینید موضوع اینه که همه ی کال بک ها داخل macrotask queue یا همون callback queue ریخته نمیشند.

فقط کال بک هایی که زمان بندی شدن و یا زمان بندی مشخصی دارند مثل کال بک های setTimeout , setInterval و event handler ها به داخل این صفِ macrotask queue ریخته میشند اما کال بک های پرامیس ها یا همون کال بک هایی که داخل then مینویسیم به محض مشخص شدن وضعیت پرامیس از web api به داخل یه صف جدا و خاص به اسم microtask queue ریخته میشند

حالا اولویت event loop برای انجام دقیقا به این ترتیبه:

  1. اولویت اولِ event loop با call stack هستش که کد های sync داخلش ریخته میشند
  2. اولویت دومِ event loop با microtask queue هستش که کال بک های پرامیس داخلش ریخته میشند.
  3. اولویت سومِ event loop با macrotask queue یا callback queue هستش که کال بک های زمان بندی شده داخل ریخته میشند.

گیف زیر خیلی روان و واضح این اولویت بندی رو نشون میده:

اولویت بندی event loop
اولویت بندی event loop


حالا یه سوال! به نظرتون خروجی کد های زیر چیه و به چه ترتیبی نمایش داده میشوند؟

پاسخ :

خط اول به دلیل sync بودن به داخل call stack ریخته میشه، خط بعدی که setTimeout هستش کال بکی که داره به web api میره و به دلیل اینکه زمان اجراش صفر ثانیه هستش درجا میره تو صف macrotask queue، خط بعدی که پرامیس هستش کال بکی که داره به web api میره و به محض resolve شدنِ وضعیتِ پرامیس، این کال بک به صف microtask queue میره، خط بعدی که کد sync هست به call stack میره.

حالا event loop با اولویت بندی که داره اول کدای داخل call stack رو اجرا میکنه، ینی اول کلمه !Start و بعدش کلمه !End نمایش داده میشه و بعدش که میبینه call stack خالی شده میره سراغ اولویت دومش که microtask queue هست و کال بک پرامیس که داخلش هست رو اجرا میکنه ینی کلمه ی !Promise نمایش داده میشه و بعد از اون میره سراغ اولویت اخرش که macrotask queue هستش و کال بکی که متعلق به setTimeout هست رو اجرا و کلمه !Timeout رو نمایش میده. بیاید این مراحل رو به صورت گیف ببینیم:


فانکشن سازنده پرامیس یا Promise constructor

شاید موقعیتی پیش بیاد که بخواید خودتون یه ابجکت پرامیس بسازید که خب برای تکمیل شدن این مقاله یه نگاهی هم به این قابلیت میندازیم:

توی مثال بالا ما پرامیسی ساختیم که مقدار resolved data رو با وضعیت Fulfilled برمیگردونه.

توی مثال بالا ما پرامیسی ساختیم که مقدار Error : rejected رو با وضعیت rejected برمیگردونه.


مبحث ()Promise.all و ()Promise.allSettled

ممکنه لیستی از ریکوست ها داشته باشیم که بخوایم همشو یکجا ریکوست بزنیم و یکجا هم مقدار همشونو بگیریم، خب متد all. از پرامیس این قابلیت رو بهمون میده و یه پارامتر که باید یک آرایه از ریکوئست هامون باشه رو میگیره و یکی یکی انجامشون میده و یه پرامیس برمیگردونه حالا اگه هیچکدوم از این ریکوئست ها به ارور نخورن با وضعیت fulfilled و یه آرایه متشکل از دیتای برگشتی از هر ریکوئست رو به عنوان نتیجه برمیگردونه:

حالا اگه حتی یکی از ریکوئست هامون ارور داشته باشه دیگه دیتای برگشتی از بقیه ریکوئست ها هم نشون نمیده و بیخیالشون میشه و فقط ارور رو نشون میده. به بیانِ دیگه این متد میگه یا همه یا هیچکدوم:

Promise.all
Promise.all

برای حل این مشکلِ "یا همه یا هیچکدوم" هم جاوااسکریپت عزیز یه متد دیگه داده بیرون به اسم ()Promise.allSettled که میاد یه آرایه برمیگردونه که داخلش برای هر ریکوئست یه ابجکت قرار داده با پراپرتی های status که مقدارش rejected یا fulfilled هست و یه پراپرتی با اسم value که مقدارش در صورت fulfilled بودن برابر با دیتای برگشتی و در صورت rejected بودن اسم این پراپرتی به reason تغییر و مقدارش برابر با متن ارور هست:

Promise.allSettled
Promise.allSettled

توی عکس بالا میبینیم که با اینکه ریکوئست دوم ارور برگردونده این متد نیومده بقیه رو هم بیخیال شه و برای هر ریکوئست، یک آبجکتی رو با ریسپانس برگشتیِ اون ریکوئست که حالا چه دیتا باشه چه متن ارور، توی آرایه قرار داده.


مبحث پرامیس ها و تمام مکملاتش اینجا به پایان میرسه و امیدوارم که به درک خوبی از نحوه هندل شدن پرامیس ها در جاوااسکریپت و همه ی موضوعات فرعی که بیان شد رسیده باشید.


خدانگهدار و موفق باشید?

promisejavascriptasynccallbackmicrotask queue
درباره ی تکنولوژی های حوزه ی فرانت اند مینویسم.
شاید از این پست‌ها خوشتان بیاید