Callback دقیقا چیست؟ چرا ما نیاز به promiseها در جاوااسکریپت داریم؟ چرا async / await انقدر سر و صدا کرده اند؟
اگر تابه حال هر کدام از سوالهای بالا را از خود پرسیدهاید، پس به مکان درستی برای گرفتن پاسخ کوتاه و غیر گیجکنندهی آنها آمدهاید.
برای گرفتن پاسختان به سراغ مثالی قابل لمس رفتهایم:
بیاید شروع به آشپزی کنیم. مراحل پخت غذای ما به شرح زیر است (به شکلی ساده شده):
- پاستا را طبخ کنید
- گوشت را طبخ کنید.
- این دو را با هم مخلوط کنید.
- کمی پنیر به مخلوط اضافه کنید.
حالا که فانکشنها تعریف شدهاند، باید آنها را فراخوانی کنیم.
خب تا به اینجای کار که بسیار آسان بوده است و چیزی که میتوانیم هم اکنون در کنسول مشاهده کنیم، لیستی از مراحلیست که انجام داده ایم.
همانطور که در تصویر بالا میبینید، عملیاتهای ما به سرعت و بااختلاف زمانی کمی، پشت سر هم انجام گرفتهاند و غذای ما بسیار سریع و درخور توجه آماده شده است. پس مثال ما خیلی واقعگرایانه نبوده است.
برای واقعیتر کردن اتفاقات و شبیهسازی آنها به شکلی که در دنیای واقعی آشپزی رخ میدهند، میتوانیم از فانکشن setTimeout استفاده کنیم. اگر با این فانکشن آشنایی ندارید، نگران نباشید؛ چرا که تنها کاری که برای ما در این مثال میکند، به تاخیر انداختن رخداد اتفاقات است. در واقع این تاخیر را با تعریف کردن در فانکشن prepare، به دو عملیات اولیه افزودهایم.
حال این بار پس از اجرا، خروجی درون کنسول به شکل زیر خواهد بود.
همان طور که مشاهده میکنید، اتفاق عجیبی این بار رخ داده است. پیش از آماده کردن پاستا و گوشت، عملیات مخلوط کردن این دو با یکدیگر فراخوانی شده.
این مثال بسیار به دنیای واقعی نزدیکتر است و مشکلی که در اینجا با آن روبرو هستیم، این است که تمام فانکشنهای ما به صورت همزمان (sync) فراخوانی شدهاند. به عبارتی دیگر جاوااسکریپت به تمام شدن پخت پاستا که مرحلهی اول آشپزی ما بوده است، بی توجه بوده و قبل از اتمام عملیات آن و پایان یافتن عملکردش، فانکشنهای بعدی را فرخوانی کرده و آنها نیز شروع به اجرا شدن کردهاند.
از آنجایی که این مثال بسیار ساده بود، امیدواریم به درستی عمق فاجعه را درک کرده باشید که این اتفاق ناخوشایند در دنیای واقعی، چه عواقبی به جای خواهد گذاشت.
Callbacks
یکی از راههای مقابله با مشکل بالا و رفع آن در دنیای برنامه نویسی، callback ها هستند. جدای از زبان برنامه نویسی مورد استفادهتان، این مفهوم اشاره به عملیاتی دارد که میتواند به عنوان آرگومان (argument) به کد شما پاس داده شود. به عبارت دیگر، callback شما، پس از انجام عملکرد اصلی کد شما، فراخوانی و اجرا خواهد شد.
بگذارید برای نشان دادن نحوهی کارکردcallback ها یک مثال خیلی ساده بیاوریم:
علاوه بر callbackای که در فانکشن prepare به عنوان آرگومان دوم پاس خواهیم داد، خود آن فانکشن جدید را نیز به نام finishCooking تعریف میکنیم که به ما زمان پایان آشپزی را اطلاع رسانی کند.
سپس دوباره تمام فانکشنهای سابق را فراخوانی میکنیم؛ تنها با این تفاوت که به دو فانکشن اول یک فانکشن callback نیز پاس میدهیم.
پس از اجرا خروجی که در کنسول مشاهده خواهیم کرد، مطابق تصویر زیر است:
به نظر میآید با استفاده از عملگر callbak، توانستیم مشکلمان را حل کنیم. اما گوشهی ذهن خود این نکته را نگه دارید که این مثال تنها برای نشان دادن مفهوم callbak ها به شما بوده است؛ پس در این مثال ساده، مشکل ما به راحتی با استفاده از callbak حل شد و اگر کمی منطق پیچیدهتری پشت کدهای ما باشد، حل این مشکل با استفاده از callbak ها کمی پیچیده و نامناسب خواهد بود و شما در آخر با یک منطقی در کد خود روبرو میشوید که در جاهای مختلفش در حال فراخوانی فانکشنهای مختلف هستید؛ پس کد شما تبدیل به یک کد ناخوانا و کثیف و حجیم شده است.
در اینجا بهتر است که با مفهوم promise آشنا شویم به جای کال بکها، از آن استفاده کنیم.
Promises
به زبان ساده promise ها یا قولها به ما کمک میکنند تا فانکشنهایمان را به اصطلاح بستهبندی (wrap) کنیم و در واقع تا زمانی که به طور کامل انجام نشدهاند و به انتهای آن فانکشن نرسیدهایم، فانکشن دیگری شروع به اجرا نکند و فراخوانی نشود. یعنی با استفاده از این قابلیت، برنامهی ما تا زمانی که فانکشن مورد نظرمان مقداری را برنگرداند، به سراغ مراحل دیگر اجرای برنامه نمیرود.
در مثال یاد شدهی ما، نیاز داریم که ابتدا تنها پاستا پخته شود و سپس بعد از اتمام پخت پاستا، گوشت پخته شود و آنگاه پس از این مراحل و اتمام کامل پخت گوشت، مخلوط کردن این دو و اضافه کردن پنیر رخ دهد.
در زیر نحوهی درست پیاده سازی promise در این برنامهی آشپزی را مشاهده میکنید که ایموجی تیک را در پاسخ موفقیت آمیز به پرامیس (resolve)، خروجی میدهیم.
همانگونه که در تصویر بالا مشاهده میشود، بعد از تعریف پرامیس مورد نظر، نتیجهی آنرا به فانکشن prepare پاس میدهیم. در اینجا برنامهی ما تا اجرای کامل فانکشن prepare بر روی pasta صبر خواهد کرد و بعد از برگردانده شدن نتیجهی آن، به سراغ اجرای دستور بعدی که فانکشن prepare بر روی bacon (گوشت) است، میرود و به همین ترتیب تا به انتها.
پس با توجه به خروجی موجود در کنسول، متوجه صحت کارکرد مراحل آشپزی خود شدهایم. پس ما منتظر میمانیم تا پاستا پخته شود و سپس (then) گوشت را طبخ میکنیم و سپس (then) دیگر موارد را انجام میدهیم. به این زنجیرهی then ها که برای کارکرد صحیح عملیاتهایمان به شکل ناهمزمان (async) تشکیل دادهایم، chaining میگویند.
این زنجیره به ما کمک میکند تا از اتمام انجام هر عملیات (resolve یا حل شدن) اطمینان حاصل کنیم و سپس به سراغ عملیاتهای بعدی برویم. البته که این نحوهی کد زدن و استفاده از پرامیسها همیشه کاربرد ندارد و باید به درستی و درجایگاه مناسب، از آن استفاده کنیم.
در مثالهای کاربردیتر، شما از هر کدام از فانکشنهای زنجیرهی promisتان، معمولا یک خروجی را به promise بعدی (به عنوان ورودی آن فانکشن) منتقل میکنید. مثلا شما میتوانید یک سری داده از یک API را fetch کنید و آن را بعد از parse (تجزیه) کردن، به فانکشن بعدی بسپارید تا عملیاتی روی آن داده انجام بگیرد. در واقع نقطهی قوت این کار و استفاده از پرامیسها، در این نکته است که اگر فانکشن اول شما با موفقیت کار خود را انجام ندهد و نتواند از API مورد نظر داده را واکشی کند و resolve نشود، فانکشن دوم نیز، به درستی، شروع به اجرا نخواهد کرد.
باز هم سعی داشتیم که مثالی ساده و ملموس برای شما در قسمت promise بیاوریم تا این مبحث را نیز به راحتی درک کنید و باید گفت که در دنیای واقعی شما باید در زنجیرهی then های خود، حالتی برای بروز ارورها نیز در نظر بگیرید و به اصطلاح خطاها را catch کنید.
اگر چه این راهکار از راهکار اول که استفاده از callbackها بود بسیار بهتر و تمیزتر به نظر میرسد، اما باز هم در برنامههای پیچیده، میتواند گمراه کننده باشد و این زنجیرهی متوالی، ممکن است شما را گیچ کند.
Async / Await
راه سوم برای حل مشکل ما، جدیدترین و بهترین روش است که در ES8 معرفی شده است. هدف این قابلیت باز هم کوتاهتر کردن و تمیزتر کردن کد ما است.
ایدهی Async Await نیز بسیار ساده است. ما نیاز داریم که یک فانکشن جدید async تعریف کنیم که به ما این اجازه را میدهد تا فانکشنهای درونی آن را بتوانیم از نظر اجرای ناهمزمان (async) تحت کنترل داشته باشیم و مشخص کنیم که کدام بخشهای آن نیاز است که حتما اجرایشان به اتمام برسد و سپس بخش بعدی کد اجرا شود. این عملکرد توسط promiseها نیز به خوبی قابل اجرا و پیادهسازی است.
پس بیایید با استفاده از این قابلیت جدید جاوااسکریپت، برای هر کدام از فانکشنهایمان، یک تاخیر را در نظر بگیریم که شبیه به دنیای واقعی شوند. برای این کار ابتدا یک فانکشن ساده میسازیم:
شبیه به کاری که در مثال promise خود کردیم، در این قسمت هم در حال ایجاد یک تاخیر و در صورت موفقتآمیز بودن آن، پاس دادن آن به ادامهی زنجیره هستیم؛ همچنین علاوهبر این کار، تمام console.log هایمان را نیز به return تبدیل کردهایم تا به استفادهی کاربردی، بیشتر نزدیک شویم. پس حالا تمام فانکشنهایمان یک چیزی را بر میگردانند.
در آخر نیز به جای فراخوانی فانکشنها به شکل تک به تک، یک فانکشن جداگانه برای اینکار میسازیم که بعدا فقط این فانکشن را فراخوانی کنیم. حال تمام چیزی که نیاز داریم که به خاطر بسپاریم، این دو نکته است:
- شما باید از کلمهی async در کنار فانکشن استفاده کنید. (این کار استفاده از await را برای شما فعال میکند و قابلیت اجرای ناهمزمان یا asynchronously را به کد شما میدهد)
- شما باید await را تنها برای فانکشنی استفاده کنید که آن عملکرد باید به اتمام برسد و مقداری را بازگرداند تا سپس برنامه به جلو برود و فانکشنهای بعدی اجرا شوند.
نکته: فراموش نکنید که از await تنها زمانی میتوانید استفاده کنید که ابتدا async را مشخص کرده باشید و همچنین سعی نکنید که از await برای فانکشنهایی در سطح بالای کد خود استفاده کنید که به عنوان مثال از اجرای تمامی فانکشنهای بعدی شما و ۹۰ درصد کد برنامهتان، جلوگیری کند.
با بررسی قطعه کد بالا، باید پروسهی زیر را درک کرده باشید:
Start cooking در کنسول چاپ میشود. سپس متغییر مربوطه، به مدت ۱۲۵۰ میلی ثانیه منتظر خواهد ماند و بعد از آن به سراغ عملکرد بعدی میرود.
به یاد داشته باشید که حالا ما در حال بازگرداندن (return) مقادیر هستیم و نه پرینت کردن آنها در کنسول. پس برای مثال متغییر combine ما، نیاز به استفاده از dish و dish2 برای مخلوط کردن آنها با یکدیگر دارد و تازمانی که آن دو را دریافت نکند، به اجرا در نخواهد آمد.
در آخر نیز یک علامت تیک را پس از نمایش المنت، چاپ خواهیم کرد که نشاندهندهی موفقیتآمیز بودن فانکشن waitTillFinish() خواهد بود.
خروجی ما در نهایت مانند زیر خواهد بود:
همانطور که میبینید تمام فانکشنها را توانستیم با تقریبا یک ثانیه تاخیر انجام دهیم. برای مخلوط کردن پاستا و گوشت نیز، دو علامت تیک دریافت کردیم که نشانگر آماده بودن پاستا و گوشت است و مخلوط کردن آنها نیز موفقیتآمیز بوده است.
شما میتوانید این مثال را در Dev tool مرورگر خود امتحان کنید و خروجیهای خود را با ما مقایسه کنید که به درک بهتری از مفاهیم گفته شده در این مطلب برسید.
حال که تا انتهای این مقاله دوام آوردهاید، شایستگی خوردن پاستای خود را دارید؛ فقط فراموش نکنید که اگر قبل از پختن پاستا و گوشت، پنیر را اضافه کنید، از دستپخت خود ناامید خواهید شد.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید