سلام دوستان?
وقتی یه زبانی رو یاد میگیرم دوس دارم برای مسلط شدن بهش بفهمم فلان مبحث در پشت پرده ی این زبان چجوری هندل میشه تا بتونم موقع استفاده ازش درک بهتری داشته باشم و در نتیجه باگ کمتری تولید کنم.
اگه شما هم جزء اون دسته برنامه نویسایی هستید که دل و روده مباحث رو تا نریزن بیرون آروم نمیگیگیرن بیاید شروع کنیم?
نکته: اگه درمورد event loop , callback queue , call stack و کلا نحوه هندل شدن توابع async در جاوااسکریپت که پیش نیاز مقاله فعلی هستند، چیزی نمیدونید، لطفا لطفا لطفا قبل شروع این مقاله ، این موارد رو یاد بگیرید که یه مقاله هم راجبشون نوشتم میتونید با کلیک روی این لینک اول اون رو مطالعه کنید و بعد به این مقاله وارد بشید.
یکی از این مباحث مهم که میخوایم دل و رودش رو بریزیم، promise ها در جاوااسکریپته. حالا چیزایی که ما قراره توی این مقاله به عنوان موضوع اصلی و مکمل هاش یاد بگیریم:
بزارید این مبحث رو با یه مثال از دنیای واقعی شروع کنیم:
پسری رو در نظر بگیرید که دو هفته دیگه تولدش هست و مادرش بهش قول میده که تا دوهفته دیگه یه کیک براش میپزه و توی این دو هفته که مادره مشغول انجام کارای کیک هست پسره مثل شلغم نمیشینه منتظر بشینه تا مادره کیک رو بپزه و بعد بره مابقی کارای تولد رو انجام بده! اینطوری همه چی بلاک و معلق میشه بخاطر یه دونه کیک :/ ، بلکه پسره طی این زمان به بقیه کاراش میرسه از جمله تهیه کردن بقیه چیزا واسه جشن تولد و... حالا این وضعیتِ قولی که مادر داده تا زمانی که تکلیفش مشخص نشده در حالت Pending یا انتظار قرار داره... وقتی تکلیفش مشخص شه اون حالت pending به یکی از دو حالت دیگه میتونه تغییر بکنه:
اگه فرض رو بر این بگیریم که مادره مریض میشه و نمیتونه کیک رو بپزه پسره قراره یه واکنشی نشون بده(ناراحت میشه) و اگه مادره اتفاق بدی نیفته و بتونه کیک رو بپزه پسره یه واکنش دیگه نشون میده (خوشحال میشه) و در نتیجه چه کیک پخته شه چه نشه یه چیز رو در نهایت هر حالتی اتفاق بیفته پسره انجام میده و اون برگزار کردن جشن هست.
عکس بالا مثالی که ما زدیم رو قشنگ نشون میده. وقتی پسره نمیشینه منتظر کیک بمونه و همزمان به بقیه کاراش میرسه به اون پروسه ای که کیک داره طی میکنه میگن asynchronous یا غیر همزمان. توی دنیای جاوااسکریپت هم اینطوری میشه گفت که این زبان به علت single thread بودن فقط میتونه یه خط کد رو در یک زمان انجام بده و نه دوتا پروسه رو همزمان!و یه زبان synchronous هست. پس پروسه های async یا غیر همزمان مثل ajax request ( گرفتن اطلاعات از سرور) در جاوااسکریپت چجوری هندل میشن؟ همون چیزی که موضوع امروز بحثمونه Promise ها ?
وقتی جاوااسکریپت به یه خط غیر همزمان مثل ajax request میخوره(fetch یا axios و...)، میدونه که این یه مقدار زمانی رو طول میکشه تا بره و دیتارو بگیره و بیاره پس بجاش یه آبجکت برمیگردونه که بهش promise یا قول گفته میشه، حالا بیاید چک کنیم این ابجکت شامل چه پراپرتی هایی هست:
توخط بالا یه ریکوست زدیم تا از سرور یه سری اطلاعات بگیریم و بریزیمش توی متغیر data اما تا این دیتا برسه یه مقدار طول بکشه پس جاوااسکریپت مثل شلغم نمیشینه بمونه تا این بره از سرور یه چیزی بگیره بیاد بعد مابقی کدهارو انجام بده چون توی این شرایط ابجکتی داره به اسم promise که اونو برمیگردونه و به بقیه کاراش میرسه. حالا این ابجکت شامل چیزایی هست که توی تصویر هم مشاهده میکنید:
3.ابجکت پروتوتایپی که اگه مقاله پروتوتایپ رو خونده باشید میدونید که این یه ابجکتیه که ایشون به ارث بردن و داخلش سه تا متد هست که وابسته به پراپرتی promiseState یا وضعیت، به طور اتوماتیک اجرا میشن که یکی یکی بررسی میکنیم:
توی مثالی که در اول مقاله زدیم هم، واکنش ناراحت شدنِ پسر به قول مادرش مثل اجرا شدن فانکشن داخلِ متدِ catch هست ،واکنش خوشحال شدنِ پسر به قول مادرش مثل اجرا شدن فانکشن داخلِ متدِ then هست، و برگزار کردن تولد در هر دو صورت مثل اجرا شدن فانکشن داخلِ متدِ finally هست.
حالا یک مثال از این متد های catch , then , finally بزنیم برای وضعیت های مختلف:
توی مثال بالا اول وضعیت پرامیس pending هستش بعدش وضعیت rejected شد(حالا به هر دلیلی) و با rejected شدن وضعیت، فانکشن یا callback داخلِ catch اجرا شد و بعدش فانکشن داخلِ finally اجرا شد.
توی مثال بالا اول وضعیت پرامیس pending هستش بعدش وضعیت resolved شد و با resolved شدن وضعیت، فانکشن یا callback داخلِ then اجرا شد و بعدش فانکشن داخلِ finally اجرا شد.
توی مثالای بالا به callback اشاره شد که شاید برای بعضیاتون سوال پیش بیاد که چی هستش؟
دوستان به فانکشنی که به عنوان آرگومان پاس داده میشه به یه فانکشن دیگه و بعد از اتمام پروسه ی داخل اون فانکشن، فانکشن پارامتر اجرا میشه callback گفته میشه.
اگه یکم بخوام برگردم عقب، وقتی جاوااسکریپت نمیتونست کارای async انجام بده و مجبور بود بخاطر single thread بودن کلی بلاک شه و معطل بمونه تا یه پروسه ی نسبتا طولانی به اتمام برسه و بتونه خط های بعدی رو اجرا بکنه. بعدش برای حل مشکل بلاک شدن روی پروسه های طولانی مدت، اینترفیس یا رابطی به اسم asynchronous به وجود اومد که با گرفتن یه فانکشن به عنوان پارامتر که همون callback خودمونه، دیگه جاوااسکریپت مجبور نبود بلاک ومعطل بمونه و در عوض میگفت در حین اینکه این پروسه طولانی انجام میشه من میرم سره بقیه کدا ولی تو بیا این کال بک رو بگیر و وقتی این پروسه تموم شد فوراََ اجراش کن.
نکته: کال بک ها asynchronous نیستند و عملا sync هستند، فقط قابلیت این رو دارند که بتونیم باهاشون کدای async اجرا بکنیم و پروسه های async یا غیرهمزمانی مثل ajax request بعد از fulfilled شدن، کال بک خودشون رو صدا میکنن تا یه بلایی سر دیتایی که برگردوندن بکنن.
بیاید یه مثال کد از کال بک ها ببینیم:
در مثال بالا وقتی getData اجرا شده ajax request رو انجام داده و پارامتر دوم یه کال بک داده تا هروقت تکلیف دیتایی که داره از این ریکوئست برمیگرده مشخص شد این کال بک رو دیتا انجام بده و پارامتری که کال بکمون میگیره همون دیتا هستش.
حالا مشکلی که این کال بک ها به وجود اوردن و باعث به وجود اومدن promise ها شدن اینه که وقتی بخوایم بلایی سر دیتا توی کال بک بیاریم که خودش async هست باید بازم مداخل اون کال بک ، کال بکِ دیگه ای رو انجام بدیم و اگه بازم داخل همون کال بک یه پروسه async دیگه بریم باز یه کال بکِ دیگه و.... زنجیره ای از کال بک ها که پشت هم پس از اون یکی اجرا میشن و این خیلی کد رو شلوغ و نامرتب میکنه که بهش callback hell گفته میشه.
توی نمونه بالا ما بعد از بدست اوردن دیتا اولین کال بک صدا زده میشه و بعد از اتمام اون کال بک، ریکوئست دوم زده میشه و کال بک بعدی انجام میشه و به ترتیب این کار چند بار یکی پشت دیگری انجام میشه و همونطور که تو عکس میبینید یه جهنم واقعی از کال بک هارو میبینید که در مقیاس های بزرگتر حتی خیلی بدتر میتونه باشه و اینکه گرفتن ارور و هندل کردنش هم توی این روش خیلی عجیب غریب و سخت تره و اینطوری شد که promise ها اومدن تا با then. و catch. کارمون رو راحتتر کنن.
پرامیس ها اومدن تا کار مارو از لحاظ پیچیدگی کال بک های تو در تو راحت کنن اما مثل اینکه کافی نبود! وقتی بخوایم مثالی که توی callback hell زدیم رو با then. و catch. پیاده کنیم اگرچه خوانا تر میشه کدمون اما ما بازم دنبال روش راحتتری هستیم. بیاید یه مثال ببینیم:
توی مثال بالا میبینید که ما یه دیتایی رومیگیریم و یه بلایی سرش میاریم (در این مثال فقط لاگ میگیریمش) اما بعدش با یه ریکوئست دیگه یه پرامیس دیگه برمیگردونیم و بعدش یک .then دیگه میزاریم و دیتای این ریکوئست جدید رو هم یه کاریش میکنیم(در این مثال لاگ میگیریم) و به همین ترتیب چندین پرامیس پشت سر هم میفرستیم و با then ها یه بلایی سرشون میاریم که به این عمل، زنجیره پرامیس ها یا promises chaining گفته میشه.
برای حل کردن مشکل زنجیره ای شدن پرامیس ها، جاوااسکریپت در es8 سینتکس جدیدی که از همون پرامیس ها استفاده میکنه معرفی کرد تا با شبیه کردن بیشترِ کدهای async به sync دیگه خیلی خیلی خوانایی کد رو راحت تر کنه.
طبق مثال بالا سینتکس 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 میره و بعد تموم میشه و اگه بلاک finally نباشه هم که بازم تمومه. حالا توی این مثال سه تای اول رو اجرا و وقتی توی jobs ارور میگیره میپره توی بلاک catch و بعد از اون بلاکِ finally.
هر خطی از کد که داخل بلاکِ try نوشتیم ارور داشته باشه دیگه جاوااسکریپت بقیه خط های داخل بلاک رو اجرا نمیکنه و یه راست میره کدی که داخل بلاک catch نوشتیم رو اجرا میکنه پارامتری که برابر با همون ارور هستش رو میگیره و میتونیم باهاش هرکاری کنیم (توی این مثال لاگ گرفتیم تا ببینیم ارور چیه) و در نهایت اگه بلاک finally وجود داشته باشه اجراش میکنه و در صورت وجود نداشتن به بقیه کاراش ادامه میده.
نکته :موقع استفاده از بلاکِ try ،استفاده از بلاک catch به همراهش ضروری هستش چون به هرحال ما داریم یه چیزی رو امتحان میکنیم تا اگه اروری داشت بگیریمش وقتی catch نباشه با چی بگیریمش؟
اما بلاکِ finally ضروری نیست و میتونیم بزاریمش یا نزاریمش.
این بلاک ها مارو توی دیباگ کردن کد ها خیلی کمک میکنن و مخصوصا موقع استفاده از async / await جای خالیِ catch. و then. رو برامون پر میکنن.
توی مقاله ی "پشت پرده ی جاوا اسکریپت! توابع 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 برای انجام دقیقا به این ترتیبه:
گیف زیر خیلی روان و واضح این اولویت بندی رو نشون میده:
حالا یه سوال! به نظرتون خروجی کد های زیر چیه و به چه ترتیبی نمایش داده میشوند؟
پاسخ :
خط اول به دلیل 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 رو نمایش میده. بیاید این مراحل رو به صورت گیف ببینیم:
شاید موقعیتی پیش بیاد که بخواید خودتون یه ابجکت پرامیس بسازید که خب برای تکمیل شدن این مقاله یه نگاهی هم به این قابلیت میندازیم:
توی مثال بالا ما پرامیسی ساختیم که مقدار resolved data رو با وضعیت Fulfilled برمیگردونه.
توی مثال بالا ما پرامیسی ساختیم که مقدار Error : rejected رو با وضعیت rejected برمیگردونه.
ممکنه لیستی از ریکوست ها داشته باشیم که بخوایم همشو یکجا ریکوست بزنیم و یکجا هم مقدار همشونو بگیریم، خب متد all. از پرامیس این قابلیت رو بهمون میده و یه پارامتر که باید یک آرایه از ریکوئست هامون باشه رو میگیره و یکی یکی انجامشون میده و یه پرامیس برمیگردونه حالا اگه هیچکدوم از این ریکوئست ها به ارور نخورن با وضعیت fulfilled و یه آرایه متشکل از دیتای برگشتی از هر ریکوئست رو به عنوان نتیجه برمیگردونه:
حالا اگه حتی یکی از ریکوئست هامون ارور داشته باشه دیگه دیتای برگشتی از بقیه ریکوئست ها هم نشون نمیده و بیخیالشون میشه و فقط ارور رو نشون میده. به بیانِ دیگه این متد میگه یا همه یا هیچکدوم:
برای حل این مشکلِ "یا همه یا هیچکدوم" هم جاوااسکریپت عزیز یه متد دیگه داده بیرون به اسم ()Promise.allSettled که میاد یه آرایه برمیگردونه که داخلش برای هر ریکوئست یه ابجکت قرار داده با پراپرتی های status که مقدارش rejected یا fulfilled هست و یه پراپرتی با اسم value که مقدارش در صورت fulfilled بودن برابر با دیتای برگشتی و در صورت rejected بودن اسم این پراپرتی به reason تغییر و مقدارش برابر با متن ارور هست:
توی عکس بالا میبینیم که با اینکه ریکوئست دوم ارور برگردونده این متد نیومده بقیه رو هم بیخیال شه و برای هر ریکوئست، یک آبجکتی رو با ریسپانس برگشتیِ اون ریکوئست که حالا چه دیتا باشه چه متن ارور، توی آرایه قرار داده.
مبحث پرامیس ها و تمام مکملاتش اینجا به پایان میرسه و امیدوارم که به درک خوبی از نحوه هندل شدن پرامیس ها در جاوااسکریپت و همه ی موضوعات فرعی که بیان شد رسیده باشید.
خدانگهدار و موفق باشید?