جاوا اسکریپت ناهمگام (Asynchronous Javascript)

آفلاین
user-avatar
عرفان حشمتی
15 بهمن 1399, خواندن در 7 دقیقه

جاوا اسکریپت ناهمگام (Asynchronous Javascript) ستون فقرات توسعه وب مدرن می‌باشد. اما درک آن همراه با چالش‌های فراوان است.

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

برای درک ناهمگامی، فقط موتور اصلی جاوا اسکریپت کافی نیست. هسته موتور JS دارای سه قسمت اصلی است:

  • رشته اجرایی
  • محیط حافظه / متغیر
  • callstack

ما می‌توانیم اجزای جدیدی مانند API مرورگر وب،Promise ها،Eventloop ، Task queue و Microtask را به آن اضافه کنیم.

همانطور که میدانید جاوا اسکریپت روی مرورگر اجرا می‌شود. خود مرورگر هم یک برنامه قدرتمند است که ویژگی‌های زیادی را فراهم می‌کند که برخی از آنها اجرای کد ناهمگام را امکان‌پذیر می‌کنند.

این ویژگی‌های مرورگر شاملDev Tools ،Console ، Sockets، درخواست‌های شبکه، رندرینگ، تایمر و موارد دیگر است. جاوا اسکریپت دارای برخی توابع است که مانند توابع معمول به نظر می‌رسند اما در واقع وسیله‌ای برای تعامل با این ویژگی‌های مرورگر وب هستند. این توابع را "facade" می‌نامیم.

نمونه‌هایی از این توابع عبارتند از:

  • شی document که بهHTML DOM اشاره دارد.
  • تابع fetch که به نوبه خود با درخواست‌های شبکه ارتباط برقرار می‌کند.
  • تابع ()setTimeOut که به تایمر اشاره دارد.

بیایید سعی کنیم قطعه کد زیر را درک کنیم.

function printHello() 
{ 
  console.log('Hello');
}
setTimeOut(printHello, 1000);
console.log('Me First');

در مرحله اول، تابع printHello در حافظه گلوبال ذخیره می‌شود. تابع facade جاوا اسکریپت یعنی setTimeOut، تنها کاری که انجام می‌دهد این است که تایمر را روی ms 1000 تنظیم می‌کند و بعد از اتمام تابع printHello را فراخوانی می‌کند.

بنابراین هنگامی که تابع setTimeOut این کار را انجام داد، به خط بعدی حرکت می‌کند و در کنسول 'Me First' را ثبت می‌کند.

در همین حال تایمر اجرا می‌شود و پس از پایان کار، تابع printHello فراخوانی می‌شود. جریان کنترل به جاوا اسکریپت اصلی بازمی‌گردد و "Hello" را چاپ می‌کند.

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

function printHello()
{
 console.log('Hello');
}
function blockfor1sec()
{ //blocks the thread for one second }
setTimeOut(printHello, 0);
blockfor1sec();
console.log('Me First');

در مثال بالا از آنجا که تایمر 0 میلی ثانیه است، می‌توان تصور کرد که printHello بلافاصله اجرا می‌شود. اما این اتفاق نمی‌افتد. چرا؟

جاوا اسکریپت قابلیتی به نام "Callback Queue" دارد. تابع printHello به درون آن رانده می‌شود و تا زمانی که موتور JS همه کدهای همزمان را اجرا نکند، در آنجا باقی می‌ماند. ویژگی دیگر به نام "Event Loop" زمانی که تمام کد گلوبال به پایان رسیده باشد، Callstack را چک می‌کند که آیا خالی است یا نه.

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

console
---------
Me first
Hello

بنابراین تابع printHello بعد از کد گلوبال اجرا می‌شود. درک این مسئله ترس از غیرقابل پیش بینی بودن هنگام نوشتن جاوا اسکریپت ناهمگام را برطرف می‌کند.

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

Promiseها

بنابراین بسیاری از کدهای جاوا اسکریپت ما با کمک ویژگی‌های مرورگر اجرا می‌شوند، اما به بک-اند آن هیچ دسترسی نداریم. ویژگی جدید Promise در ES6 به ما کمک می‌کند تا نوعی سازگاری بین موارد موجود در پس زمینه اجرا و حافظه جاوا اسکریپت واقعی خود داشته باشیم. در ES5 ما از fetch/xhr برای دریافت درخواست در اینترنت استفاده می‌کردیم اما بر حافظه واقعی تاثیری نداشت. با معرفی Promises در ES6، واکشی باعث ایجاد درخواست شبکه می‌شود، در حالی که هنوز یک شی Promise را که در حافظه است، برمی‌گرداند. پس از پایان درخواست، شی Promise با داده‌های درخواست پر می‌شود.

Promiseها را می‌توان با در نظر گرفتن دو رویکرد بررسی کرد. رویکرد اول به فرایندهای پس زمینه‌ای که از طریق ویژگی‌های مرورگر اجرا می‌شوند اشاره می‌کند. در حالی که رویکرد دوم به شی Promise در حافظه گلوبال اشاره دارد که درخواست را ردیابی می‌کند.

استفاده از این دو تابع facade کمک می‌کند تا:

  • فرایند پس زمینه مرورگر وب را آغاز کنید.
  • بلافاصله در جاوا اسکریپت یک شی promise را برگردانید.
function display(data)
{
  console.log(data);
}
const futureData = fetch('https//somelink.com');
futureData.then(display);
console.log('Me first');

قطعه کد فوق promise ساده‌ای را نشان می‌دهد. مقادیری که از درخواست دریافت می‌کنیم در futureData ذخیره می‌شود و برای نمایش تابع به عنوان یک پارامتر (داده) به آن منتقل می‌شود. برای درک اینکه چگونه همه این موارد خودکار انجام می‌شود، باید بفهمیم واکشی دقیقا چه کار می‌کند؟

تابع Fetch یکی از مهمترین توابع جاوا اسکریپت است. این تابع بلافاصله دو رویکردی را که قبلا توضیح دادیم، اجرا می‌کند.

  • یک شی promise را در حافظه جاوا اسکریپت تنظیم می‌کند. این شی دارای دو ویژگی است: مقداری که داده‌های پاسخ را ذخیره می‌کند و onFulfilled که یک آرایه خالی است.
  • از طرف دیگر این ویژگی همچنین قابلیت مرورگر "درخواست شبکه" را برای ارسال درخواست http به دامنه و دریافت اطلاعات پاسخ فعال می‌کند. سپس در متغیری که درخواست در آن ذخیره شده است یعنی futureData و در شی promise آن قرار می‌گیرد. futureData.value = response data

همچنین باید آنچه را که هدف onFulfiled[] (ویژگی پنهان شی Promise) است بررسی کنیم، اما نمی‌توانیم به آن دسترسی داشته باشیم. به محض این که شی promise مقداری با ارزش را به دست آورد، ویژگی onFulfilled به طور خودکار تابعی را اجرا می‌کند که قرار بود از داده‌های پاسخ استفاده کند. این تابع در آرایه onFulfiled ذخیره می‌شود. در قطعه کد بالا، به محض اینکه ویژگی مقدار داده‌های پاسخ را به دست آورد، تابع display توسط onFulfilled آغاز شده و داده‌های پاسخ به عنوان آرگومان به آن منتقل می‌شوند.

در این مرحله، مسلما باید این پرسش مطرح شود که چگونه می‌توان تابعی را به آرایه onFulfiled اضافه کرد؟ ما نمی‌توانیم از ()array.push استفاده کنیم، زیرا این یک ویژگی پنهان است. در عوض از متد "then" استفاده می‌کنیم که کار را بسیار ساده می‌کند.

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

همچنین می‌توانیم از متد ".catch()" برای افزودن تابعی به آن استفاده کنیم. این تابع توسط onRejection فعال شده و در صورت بروز خطا اجرا می‌شود.

سرانجام باید بدانیم که چگونه Promise با تأخیر به جاوا اسکریپت بازمی‌گردد تا اجرا شود؟ در جاوا اسکریپت یک ویژگی مشابه صف callback وجود دارد که "صف Microtask" نامیده می‌شود. هنگامی که درخواست شبکه در حال انجام و واکشی داده‌ها است، تابع مرتبط با شی Promise به صف microtask رانده می‌شود.

به محض پر شدن داده‌های پاسخ، تابع از صف microtask به بالا کشیده می‌شود و توسط حلقه رویداد روی callstack پاپ (اضافه) شده و اجرا می‌شود.

صف Microtask از اولویت بیشتری نسبت به صف Callback برخوردار است. بنابراین توابع مرتبط با سایر ویژگی‌های ناهمگام مانند ()setTimeOut پس از اجرای توابع شی Promise اجرا می‌شوند.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان حشمتی @heshmati74
مهندس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو