درک بسیار ساده async در جاوااسکریپت

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 08 تیر 1398
دسته بندی ها : جاوا اسکریپت

اوایل که قصد یادگیری برنامه‌نویسی asynchronous در جاوااسکریپت را داشتم، مدام موضوعات مختلف را با هم اشتباه می‌گرفتم و نمی‌توانستم مفهوم کلی آن را به یاد بسپارم. در کنار این، تلاش بسیار زیادی کردم تا تفاوت‌ بین promises و async/await را درک کنم، اما در نهایت متوجه شدم که تلاش‌های‌م چندان مفید نبوده‌اند چرا که در نهایت این دو کاملا به همدیگر مربوط هستند و تا حدی می‌شود گفت که یک مقوله را ارائه می‌کنند.

قابلیت Async در جاوااسکریپت در طی سال‌ها تغییرات بسیار زیادی به خود دیده است. به همین دلیل است که آموزش‌های مختلف، به شیوه‌های متفاوتی در این ارتباط صحبت می‌کنند. نتیجه مشاهده آموزش‌های متفاوت نیز من را بیش از پیش دچار سردرگمی کرد. اما من تصمیم گرفتم تا با نوشتن یک مطلب، توضیحی بسیار ساده را از مقوله Async در جاوااسکریپت ارائه دهم. مطلبی که بتواند به صورت بصری و با کلامی بسیار ساده، درکی کامل را از Async به مخاطبین بدهد. 

چرا ما به Async نیاز داریم؟

جاوااسکریپت به صورت ریشه‌ای، یک زبان برنامه‌نویسی synchronous، blocking و تک-رشته‌ای است. اگر چنین کلماتی برای شما معنایی ندارند مشکلی نیست، در زیر تلاش می‌کنم تا در یک تصویر به خوبی این موضوع را به شما نشان دهم:

در تصویر بالا خطوط آبی میزان زمانی را نشان می‌دهند که برنامه برای اجرا صرف کرده است، خطوط قرمز نیز میزان زمانی است که برنامه برای ارتباط برقرار کردن منتظر مانده است.

ما قصد داریم تا از متدهای Async برای کارهایی استفاده بکنیم که در پس‌زمینه اجرا می‌شوند. دلیل استفاده از چنین متدی آن است که ما نمی‌خواهیم در زمان اجرای یک کوئری یا درخواست API کل اپلیکیشن‌مان از کار بیافتد. در دنیای واقعی این حالت زمانی اتفاق می‌افتد که شما برای انجام هیچ کاری آماده نیستید. نمی‌توانید به تماس‌ها جواب بدهید، نمی‌توانید غذا بخورید و یا لباس های‌تان را بشویید. چنین موضوعی ممکن است تمام زندگی شما را مختل بکند، همانطور که باعث ایجاد اختلال در کار با یک نرم‌افزار می‌شود.

همانطور که اشاره شد جاوااسکریپت به صورت اصلی یک زبان Synchronous است، اما راه‌هایی نیز وجود دارد که باعث می‌شود تا به صورت Asynchronous کار بکند. 

سیر تکاملی Async

زمانی که در اینترنت به دنبال «Async JS» می‌گردم، پیاده‌سازی‌های متفاوتی را مشاهده می‌کنم: callback، Promise و async/await. برای من بسیار مهم بود که هر کدام از این متدها را به خوبی درک بکنم و بدانم که دقیقا چه معنایی دارند. در این قسمت از مطلب قصد دارم تا نتیجه کاری که انجام دادم را به اشتراک بگذارم.

Callback

قبل از بوجود آمدن ES6 ما قابلیت Async را با استفاده از Callbackها ایجاد می‌کردیم. نمی‌خواهم که به صورت خیلی عمیق در ارتباط با این موضوع صحبت کنم اما یک Callback در واقع تابعی‌ست که به عنوان پارامتر تابعی دیگر فراخوانی شده و زمانی اجرا می‌شود که روند اجرای تابع کنونی به پایان رسیده باشد. برخی از توسعه‌دهندگان به بلوک کلی یک Callback می‌گویند «Callback Hell» که به معنی «جهنم Callback» است.

برای پیاده‌سازی چنین قابلیتی شما نیاز دارید یک زنجیره از رویدادها را ایجاد کنید، برای انجام چنین کاری در Callback یکسری توابع را به صورت تو در تو ایجاد می‌کنیم.

از آنجایی که پیاده‌سازی چنین حالتی به معنای واقعی کلمه سردردآور و پیچیده بود، جامعه جاوااسکریپتی تصمیم گرفتند تا Promise را ارائه کنند.

Promises

ما انسان‌ها قابلیت بسیار خوبی را در خواندن کدهای Synchronous داریم، حال Promise قصد دارد که یک Async را ایجاد کند اما ظاهر کدها به صورت یک برنامه Synchronous باشد. یک شکل کلی از این حالت را می‌توانید در تصویر زیر مشاهده بکنید:

البته در کدهای بالا یک المان مهم فراموش شده و آن هم قابلیت مدیریت خطا است. تا به حال شده که با خطای unhandledPromiseRejection روبرو شوید؟ این اتفاق در حالتی می‌افتد که Promise به جای اجرا شدن، reject یا رد می‌شود.

برای حل چنین مشکلی نیاز است تا یک حالت را برای catch در نظر بگیریم. اینگونه می‌شود در صورت برخورد با خطا، حافظه را آزاد کرده و پیغام مربوطی را نشان دهیم.

Async/Await

همانطور که گفته شد Async/Await و Promiseها دو روح در قالب یک جسم هستند. استفاده از Asyn/Await باعث می‌شود تا کدهای شما خواناتر شود. زمانی که کلمه کلیدی async را به تابع اضافه می‌کنیم، طبیعت ذاتی آن تغییر می‌کند. 

یک تابع async، مقداری را در داخل یک promise برگشت می‌دهد. حال برای دسترسی پیدا کردن به آن مقدار ما یا از تابع .then و یا از await استفاده می‌کنیم.

برای آنکه تفاوت این دو حالت را در دسترسی به یکسری داده بهتر متوجه شویم، من در قالب یکسری مثال هر دو حالت را نشان می‌دهم. 

مقایسه Promises و Async/Await

در سمت چپ تصاویر ما از Promiseها استفاده کرده ایم و در سمت راست از Async/Await.

فراخوانی

getJSON() تابعی است که یک Promise را برمی‌گرداند. حال برای قسمت سمت چپ در جهت اجرای Promise از متد .then و یا .catch استفاده می‌کنیم. اما در سمت راست await را به کار برده‌ایم. به عنوان یک نکته مهم، باید بدانید که await تنها زمانی می‌تواند فراخوانی شود که در یک تابع async قرار بگیرد.

ایجاد

هر دو دستور بالا خروجی Promise {<resolved>: "hi"} را خواهند داشت. Resolve یکی از توابع اجرایی برای promiseهاست. زمانی که آن را فراخوانی کنید یک شئ promise برگشت داده خواهد شد. در حالت async ما از یک arrow function استفاده کرده‌ایم که در نتیجه به صورت سریع خروجی را برمی‌گرداند.

مدیریت خطا

راه‌هایی برای دریافت خطاها وجود دارد. یکی از آن‌ها استفاده از then/catch و حالتی دیگر استفاده از try/catch است. می‌توان برای هر دو حالت Promise و Async/Await از این موارد استفاده کرد اما معمولا برای Promiseها از then/catch و برای Async/Await از try/catch استفاده می‌شود.

یکی از مزیت‌های بزرگ استفاده از Async/Await برای مدیریت خطاها، در error stack trace یا ردگیری پشته خطا است. ردگیری پشته در واقع یک گزارش از پشته‌ای است که در یک زمان خاص در حال اجرا شدن است. در Promise زمانی که تابع B اجرا شود ما دیگر به تابع A در Stack Trace دسترسی نداریم. اما Async/Await در هر حالتی به A دسترسی خواهیم داشت. 

حال که مهمترین سناریوها را برای درک تفاوت این دو مورد بررسی کردیم، بیایید با چند مفهوم مهم دیگر آشنا شویم.

Async متوالی در مقابل موازی «Sequential vs Parallel»

از آنجایی که Async/Await یک سینتکس را بسیار خواناتر می‌کنند، ممکن است گاهی اوقات کاربر را بین اینکه کدها به صورت موازی یا متوالی اجرا می‌شوند درگیر کند. در این قسمت می‌شود تفاوت آن‌ها را درک کرد.

اجرا به صورت موازی

بیایید فرض کنیم که شما کارهای بسیار زیادی را در طول روز باید انجام دهید: یک ایمیل جدید درست کنید، لباس‌ها را بشورید و  به چند ایمیل و پیغام جواب بدهید. از آنجایی که این موارد ارتباطی به همدیگر ندارند شما می‌توانید تمام آن‌ها را از طریق یک Promise.all() اجرا کنید. در واقع در این صورت Promise.all() یک آرایه را گرفته و سعی می‌کند تا تمام کارها یا متدها را اجرا کند.

اجرای متوالی

در حالت دیگر، شما نیاز به انجام کارهایی دارید که به همدیگر مرتبط هستند و باید به صورت ترتیبی انجام شوند. برای مثال شما ابتدا نیاز است تا لباس‌ها را بشورید، سپس خشک کنید و بعد از آن آن‌ها را اتو نمایید. این کارها به صورت همزمان انجام پذیر نیستند. اجرای متوالی نیز دقیقا به همین صورتند.

این توابع به صورت نوبتی اجرا می‌شوند چرا که خروجی هر مورد، تابعی است برای دستور بعدی. بنابراین تابع دوم باید تا زمان اجرای دستورات قبلی خود صبر کند. این روال برای تمام خطوط به همین صورت است.

نکته‌ای برای یادگیری

زمانی که تصمیم به یادگیری این مباحث کردم با ویدیوهای آموزشی بسیار زیادی برخورد کردم که عملا هیچ کدام آن‌ها را متوجه نمی‌شدم. بعد از مدتی به این نتیجه رسیدم که برای برخی از موضوعات، من باید روند و استایل متفاوتی را برای یادگیری انتخاب کنم. به همین دلیل سراغ آموزش‌های بصری و کارتونی رفتم. از این طریق بود که توانستم نکات مهمی را یاد بگیرم.

منبع

مقالات پیشنهادی

  • چرا Async؟ جاوااسکریپت و دنیای واقعی

    در واقع، ممکن است که قبلا عبارت «callback hell» را شنیده باشید. این عبارت بی دلیل ساخته نشده بود: کدی که بر پایه callback باشد، به ناچار در جایی ختم م...

    عرفان کاکایی