معرفی جاوا اسکریپت ناهمگام – به زبان ساده


در این مقاله به اختصار به بررسی مشکلات مرتبط با جاوا اسکریپت ناهمگام میپردازیم. همچنین برخی از تکنیکهای مختلف برنامهنویسی ناهمگام که میتوان مورد استفاده قرار دارد را بررسی میکنیم و نشان میدهیم که این تکنیکها چگونه میتوانند به حل برخی از مسائل کمک کنند. برای مطالعه بخش قبلی به لینک زیر رجوع کنید.
پیشنیازها
- سواد مقدماتی رایانه
- درکی نسبی از مبانی جاوا اسکریپت
هدف از این مقاله آشنا ساختن مخاطب با جاوا اسکریپت ناهمگام، تفاوت آن با جاوا اسکریپت همگام و کاربردهای آن است.
جاوا اسکریپت همگام
برای این که بتوانیم معنی جاوا اسکریپت «ناهمگام» (Asynchronous) را بدانیم باید ابتدا مطمئن شویم که معنی جاوا اسکریپت «همگام» (Synchronous) را میدانیم. در این بخش برخی از اطلاعاتی که در مقاله قبلی این سری ارائه شده است را جمعبندی میکنیم.
بخش عمدهای از کارکردهایی که در بخش قبلی این سری آموزشی مشاهده کردیم در واقع تکنیکهای برنامهنویسی همگام بودند. در این روش شما کد اجرا میشود و نتیجه به محض این که مرورگر بتواند کد را اجرا کند بازگشت مییابد. به مثال ساده زیر توجه کنید:
در این مثال یک دکمه وجود دارد که پس از یک میلیون بار محاسبه تاریخ، عبارتی را روی صفحه نمایش میدهد. آن را عملاً در این آدرس (+) مشاهده کنید.
در بلوک کد فوق کارهای زیر یکی پس از دیگری اجرا میشوند:
- یک ارجاع به عنصر <button> به دست میآید که در DOM قرار دارد.
- یک شنونده رویداد اضافه میشود که وقتی دکمه کلیک شد، کارهای زیر را انجام میدهد:
- یک پیام هشدار ()alert ظاهر میشود.
- زمانی که هشدار بسته شود، یک عنصر <p> ایجاد میشود.
- سپس نوعی محتوای متنی به آن اضافه میشود.
- در نهایت پاراگراف به بدنه سند اضافه خواهد شد.
زمانی که هر کدام از این عملیات در حال پردازش هستند، هیچ کار دیگری اجرا نخواهد شد و به این ترتیب رندر کردن صفحه متوقف میشود. دلیل این مسئله را در بخش قبلی این سری مقالات گفتیم و این است که جاوا اسکریپت یک زبان تک نخی است. در هر زمان تنها یک کار میتواند اجرا شود که روی نخ main اجرا میشود و هر چیز دیگری تا زمان تکمیل شدن آن عملیات متوقف خواهد بود.
بنابراین در مثال فوق، پس از این که روی دکمه کلیک کردید، پاراگراف ظاهر نمیشود تا این که دکمه OK را در کادر هشدار کلیک کنید. میتوانید آن را خودتان بررسی کنید:
نکته: به خاطر داشته باشید که ()alert گرچه برای نمایش عملیات انسداد همگام مفید است، اما استفاده از آن در اپلیکیشنهای واقعی فاجعه محسوب میشود.
جاوا اسکریپت ناهمگام
به دلایلی که در بخش قبلی نشان دادیم، بسیاری از قابلیتهای API وب به خصوص آنهایی که به نوعی منابع دسترسی مییابند یا آن را از یک محل خارجی واکشی میکنند، هم اینک از کدنویسی ناهمگام برای اجرا استفاده میکنند. برای نمونه واکشی فایل از شبکه یا دسترس به پایگاه داده و بازگشت داده از آن، دسترسی به استریم ویدئو از یک دوربین وب یا پخش تصاویر نمایشگر روی یک هدست VR شامل این عملیات میشود.
چرا عادت به کار با کد ناهمگام دشوار است؟ برای پاسخ به این سؤال یک مثال ساده را بررسی میکنیم. وقتی یک تصویر را از سرور واکشی میکنید، نمیتوانید بیدرنگ نتیجهای را بازگشت دهید. این بدان معنی است که شِبه کد زیر عملی نخواهد بود:
دلیل این مسئله آن است که نمیدانید چه قدر طول میکشد تا تصویر دانلود شود و از این رو زمانی که خط دوم اجرا شود (به احتمال زیاد در تمام موارد) خطایی ایجاد میشود، چون response هنوز موجود نیست. به جای آن باید از کد خود بخواهید صبر کند تا response بازگشت یابد و سپس تلاش کند تا هر کاری دیگری با آن انجام دهد.
دو نوع عمده از سبک کد ناهمگام وجود دارد که در جاوا اسکریپت با آن مواجه میشویم. یکی Callback-های سبک قدیمی است و دیگری کد به سبک Promise. در ادامه این مقاله هر کدام از آنها را به نوبت بررسی میکنیم.
Callback-های ناهمگام
Callback-های ناهمگام تابعهایی هستند که در زمان فراخوانی یک تابع به صورت پارامتر استفاده میشوند و شروع به اجرا در پس زمینه میکنند. زمانی که کد پس زمینه اجرای خود را تمام کند، تابع Callback را فراخوانی میکند تا بداند که کار انجام یافته است و یا اطلاع دهد که اتفاق خاصی رخ داده است. استفاده از Callback روشی نسبتاً قدیمی محسوب میشود، گرچه همچنان در برخی API-های قدیمیتر اما رایج استفاده میشود.
نمونهای از یک Callback ناهمگام، پارامتر دوم ()addEventListener است که در بخش قبلی دیدیم:
پارامتر نخست نوع رویدادی است که منتظر وقوع آن هستیم و پارامتر دوم یک تابع Callback است که در زمان اجرای رویداد احضار میشود.
زمانی که یک تابع Callback را به صورت یک پارامتر به تابع دیگری ارسال میکنیم، صرفاً تعریف تابع را به صورت پارامتر میفرستیم و تابع Callback بیدرنگ اجرا نخواهد شد. نام آن Callback یعنی فراخوانی بازگشتی است و برحسب نام خود به صورت ناهمگام جایی در بدنه تابع قرار گرفته و در زمان مقتضی اجرا خواهد شد. تابعی که Callback را در خود احاطه کرده، مسئول اجرای تابع Callback در زمان مقتضی است.
شما میتوانید تابعهای خاصی را بنویسید که شامل Callback باشند. در مثال زیر یک تابع Callback را میبینیم که منبعی را از طریق API به نام XMLHttpRequest بارگذاری میکند:
ما در این کد یک تابع به نام ()displayImage ایجاد کردهایم که یک blob ارسالی را به صورت یک URL شیء را نمایش میدهد و سپس یک تصویر ایجاد میکند تا URL را در آن نمایش دهد و آن را به <body> سند الصاق میکند.
با این حال، در ادامه یک تابع ()loadAsset ایجاد میکنیم که یک Callback به عنوان پارامتر میگیرد و همراه با آن یک URL برای واکشی و نوع محتوا را نیز دریافت میکند. این تابع از XMLHttpRequest که عموماً به اختصار XHR نامیده میشود، برای واکشی منبع از URL مفروض استفاده میکند و سپس پاسخ را در Callback ارسال میکند تا هر کاری که لازم است روی آن اجرا کند. در این حالت، callback روی درخواست XHR منتظر میماند تا دانلود کردن منبع به پایان برسد. این کار با استفاده از دستگیره رویداد onload صورت میپذیرد و سپس تصویر را به Callback ارسال میکند.
Callback-ها متنوع هستند و نه تنها امکان کنترل ترتیب اجرای تابعها و این که چه دادههایی به آنها ارسال میشوند را دارند، بلکه امکان فرستادن دادهها به تابعهای مختلف بر اساس شرایط خاص را نیز فراهم میسازند. بنابراین میتوانید کارهای مختلفی مانند ()processJSON() ،displayText و غیره برای اجرا روی یک پاسخ دانلود شده تعریف کرد.
توجه داشته باشید که همه Callback-ها ناهمگام نیستند و برخی از آنها به صورت همگام اجرا میشوند. به عنوان مثال، زمانی که از ()Array.prototype.forEach برای تعریف حلقه روی آیتمهای یک آرایه استفاده میکنید، در واقع از یک Callback همگام استفاده کردهاید.
در این مثال، روی یک آرایه متشکل از نام خدایان یونان باستان حلقهای تعریف کردیم و شماره اندیس و مقدار آنها را در کنسول نمایش میدهیم. پارامتر مورد انتظار ()forEach یک تابع Callback است که خودش دو پارامتر میگیرد که یکی ارجاعی به نام آرایه و دیگری مقدار اندیسهاست. با این حال این تابع منتظر چیزی نمیماند و بیدرنگ اجرا میشوند.
Promise-ها
Promise-ها سبک جدیدی از کد ناهمگام هستند که در API-های مدرن وب مشاهده میشوند. مثال خوبی از آن در API به نام ()fetch دیده میشود که اساساً نسخه مدرنتر و کارآمدتری از XMLHttpRequest است. در ادامه مثال کوچکی را ملاحظه میکنید که دادهها را از سرور واکشی میکند:
در کد فوق ()fetch یک پارامتر منفرد میگیرد که URL منبعی است که میخواهیم از شبکه واکشی کنیم و یک Promise بازگشت میدهد. Promise شیئی است که تکمیل یا شکست عملیات ناهمگام را نمایش میدهد. این پارامتر یک حالت واسط را نمایش میدهد. در واقع این روشی است که مرورگر استفاده میکند تا اعلام کند: «من قول میدهم به زودی با پاسخ بازخواهم گشت» و از این رو نام آن Promise یعنی «قول» است.
عادت به استفاده از این مفهوم نیاز به تمرین کردن دارد. در عمل کمی شبیه به گربه شرودینگر است. هیچ کدام از وضعیتها هنوز اتفاق نیفتادهاند و از این رو عملیات واکشی در حال حاضر منتظر نتیجه عملیات مرورگر است تا عملیات خود را در زمانی در آینده به پایان ببرد. در ادامه سه بلوک کد دیگر نیز داریم که در انتهای ()fetch قرار دارند:
- بلوکهای ()then: هر دو این بلوکها شامل یک تابع Callback هستند که در صورت موفق بودن عملیات قبلی اجرا میشوند و هر Callback یک ورودی در نتیجه موفق بودن عملیات قبلی میگیرد، به طوری که میتواند به پیش برود و کار دیگری را اجرا کند. هر بلوک ()then. یک Promise دیگر بازگشت میدهد، یعنی میتوان چند بلوک ()then را به هم زنجیر کرد به طوری که چند عملیات ناهمگام به ترتیب و یکی پس از دیگری اجرا شوند.
- بلوک ()catch: در انتهای کد در صورتی اجرا میشود که بلوکهای ()then ناموفق باشند. این وضعیت شبیه به بلوکهای try...catch همگام است که در آن یک شیء خطا درون ()catch قرار میگیرد و میتواند برای گزارش نوع خطایی که رخ داده است مورد استفاده قرار گیرد. توجه کنید که گرچه آن try...catch همگام در مورد Promise-ها جواب نمیدهد، اما با ساختار async/await که در ادامه معرفی خواهیم کرد کار میکند.
در بخشهای بعدی این سری مقالات آموزشی با Promise-ها بیشتر آشنا خواهید شد و اگر اکنون نکته مبهمی برایتان وجود دارد لازم نیست نگران باشید. کافی است به ادامه مطالعه مقالههای این سری بپردازید.
صف رویداد
عملیات ناهمگام مانند Promise در یک «صف رویداد» (event queue) قرار میگیرد که پس از پایان پردازش نخ اصلی اجرا میشود، به طوری که کد جاوا اسکریپت بعدی را مسدود نمیکند. عملیات صفبندی شده به محض این که امکانپذیر باشد، اجرا میشوند و نتایج آنها در محیط جاوا اسکریپت بازگشت مییابد.
مقایسه Promise با Callback
Promise-ها مشابهتهایی با Callback های سبک قدیم دارند. آنها اساساً یک شیء را بازگشت میدهند که به جای الزام به ارسال Callback به یک تابع، به تابعهای Callback الصاق مییابند.
با این حال، Promise-ها به طور اختصاص برای مدیریت عملیات ناهمگام ساخته شدهاند و مزیتهای زیادی نسبت به Callback-های قدیمی دارند که در فهرست زیر به برخی از آنها اشاره کردهایم:
- شما میتوانید چند عملیات ناهمگام را با استفاده از چند عملیات ()then. به هم زنجیر کنید و نتیجه یکی را به عنوان ورودی به دیگری ارسال کنید. اجرای این کار با Callback-ها بسیار دشوارتر است و در اغلب موارد به وضعیتی به نام «هرم مرگ» یا «جهنم Callback» منتهی میشود.
- Callback-های Promise همواره با ترتیب مشخصی که در صف رویداد قرار گرفتهاند فراخوانی میشوند.
- مدیریت خطا بسیار بهتر است، چون همه خطاها از سوی یک بلوک منفرد ()catch. در انتهای بلوک کد مدیریت میشوند و دیگر لازم نیست به صورت منفرد در هر سطح از هرم خطایابی شود.
ماهیت کد ناهمگام
در ادامه مثال دیگری را بررسی میکنیم که ماهیت کد ناهمگام را بیشتر روشن میسازد و نشان میدهد که وقتی از ترتیب اجرای کد به درستی آگاه نباشیم و تلاش کنیم با کد ناهمگام همانند کد همگام برخورد کنیم، چه نوع مشکلاتی میتوانند بروز یابند. مثال زیر تا حدودی مشابه آن چیزی است که قبلاً دیدیم. یک تفاوت آن است که ما در این کد تعدادی گزاره ()console.log نیز گنجاندهایم که ترتیب کدی که شما ممکن است فکر کنید اجرا میشود را نمایش میدهد.
مرورگر کار خود را با اجرای کد آغاز میکند و نخستین گزاره ()console.log یعنی پیام «Starting» را میبینید و آن را اجرا میکند، سپس متغیر image را ایجاد میکند.
در ادامه به خط بعدی میرود و شروع به اجرای بلوک ()fetch میکند، اما از آنجا که ()fetch به صورت ناهمگام بدون مسدودسازی اجرا میشود، اجرای کد پس از کد مبتنی بر Promise ادامه مییابد و بدین طریق به گزاره ()console.log نهایی میرسد و خروجی یعنی پیام «!All done» را در کنسول ارائه میکند.
تنها زمانی که بلوک ()fetch به صورت کامل پایان یابد و نتیجهاش را از طریق بلوکهای ()then. ارائه کند، در نهایت پیام ()console.log دوم یعنی «(;It worked» ظاهر میشود. بنابراین پیامها در ترتیبی متفاوت از آن چه احتمالاً انتظار داشتید ظاهر میشوند:
- Starting
- All done!
- It worked:)
اگر این وضعیت موجب سردرگمی شما شده است، مثال کوچکتر زیر را در نظر بگیرید:
رفتار این بلوک کد کاملاً مشابه است، پیامهای اول و سوم ()console.log بیدرنگ نمایش پیدا میکنند، اما گزاره دوم مسدود میشود تا این که دکمه ماوس را کلیک کنید. مثال قبلی نیز به روش مشابهی عمل میکند به جز این که به جای کلیک کردن ماوس، کد پیام دوم مسدود میشود تا زنجیره Promise منبعی را واکشی کرده و آن را روی صفحه نمایش دهد.
در یک مثال از کدی که پیچیدهتر از این دو است، این تنظیمات میتواند موجب بروز مشکل شود، چون نمیتوان هیچ بلوک کد ناهمگامی را که نتیجهای بازگشت میدهد و نتیجه آن در ادامه مورد نیاز خواهد بود در بلوک کد ناهمگام گنجاند. نمیتوان تضمین کرد که تابع ناهمگام پیش از این که مرورگر بلوک همگام را پردازش بکند بازگشت خواهد یافت.
برای این که این مشکل را در عمل مشاهده کنیم ابتدا یک کپی از کد زیر روی سیستم خود ایجاد کنید:
سپس فراخوانی ()console.log را به صورت زیر تغییر دهید:
اینک باید به جای پیام سوم، یک خطا در کنسول مشاهده کنید:
TypeError: image is undefined; can't access its "src" property
دلیل این امر آن است که مرورگر تلاش میکند، گزاره ()console.log سوم را اجرا کند و بلوک ()fetch هنوز اجرای خود را تمام نکرده است و از این رو متغیر image هنوز مقداری ندارد.
یادگیری عملی: همه کدها را ناهمگام بنویسید
برای حل مشکلی که در مثال ()fetch دیدیم و برای این که گزاره ()console.log سوم در ترتیب مطلوب نمایش پیدا کند، باید کاری کنیم که گزاره ()console.log سوم نیز به صورت ناهمگام اجرا شود. این کار از طریق انتقال آن به درون بلوک ()then. که به انتهای دومی زنجیر شده است، امکانپذیر خواهد بود. همچنین میتواند به سادگی آن را به درون بلوک ()then سوم برد. تلاش کنید این مشکل را به این ترتیب اصلاح کنید.
نکته: اگر با مشکل مواجه شدید میتوانید از کد زیر کمک بگیرید:
سخن پایانی
جاوا اسکریپت در ابتداییترین شکل خود یک زبان برنامهنویسی همگام، مسدودکننده و تک نخی است بدین معنی که در این زبان در هر لحظه تنها یک عملیات اجرا میشود. اما مرورگرهای وب تابعها و API-هایی تعریف میکنند که امکان ثبت تابعهایی که باید به صورت ناهمگام اجرا شوند را میدهند. بدین ترتیب میتواند این تابعها را به صورت ناهمگام در زمانی که رویداد خاصی اتفاق افتاد مانند گذشت زمان معین، تعامل کاربر با ماوس یا رسیدن دادهای از شبکه، اجرا کرد. این به آن معنی است که میتوان اجازه داد کد چندین کار را همزمان اجرا کند و در عین حال نخ اصلی نیز مسدود یا متوقف نشود.
این که بخواهیم کد خود را به صورت ناهمگام و یا همگام اجرا کنیم به کاری که قرار است انجام دهیم وابسته است. مواردی وجود دارند که میخواهیم چیزی بیدرنگ بارگذاری و اجرا شود. برای نمونه زمانی که نوعی استایل تعریف شده از سوی کاربر را روی یک صفحه وب اعمال میکنیم میخواهیم که این کار در سریعترین حالت ممکن اجرا شود.
اما اگر عملیاتی اجرا میکنیم که زمانبر خواهد بود مثلاً به پایگاه داده کوئری میزنیم و از نتایج آن برای ایجاد قالب استفاده میکنیم بهتر است آن را از نخ اصلی خارج کنیم و این کار را به صوت ناهمگام به پایان ببریم. شما در طی زمان خواهید آموخت که چه زمانی باید از تکنیکهای ناهمگام و چه هنگام از کدهای همگام استفاده کنید. برای مطالعه بخش بعدی این مطلب به لینک زیر رجوع کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکریپت
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مفاهیم عمومی برنامه نویسی ناهمگام (Asynchronous Programming) — به زبان ساده
- رایج ترین روش های ایجاد درخواست HTTP در جاوا اسکریپت — راهنمای مقدماتی
==