جاوا اسکریپت چگونه کار می‌کند؟ نگاهی به Runtime Environment

در این مقاله نگاهی به محیط اجرایی جاوا اسکریپت ( ترجمه‌ی سخت Runtime environment) در بستر مرورگر میندازیم و یاد میگیریم که موتور جاوا اسکریپتِ V8 گوگل، چطور کدها رو تحلیل و تجزیه ( Parse ) می‌کنه و همچنین با نقش حلقه ی رویداد ( Event Loop ) در شرایط Single Thread ، چه به شکل خطی ( synchronously ) و "یک جورایی" غیر خطی ( asynchronously ) آشنا می‌شیم.از اولش شروع کنیم ..هر مرورگر ( مثل کروم، فایرفاکس یا سافاری ) یک محیط اجرایی جاوا اسکریپت داره که یکسری API ها رو در اختیار توسعه دهنده/برنامه نویس میذاره، چیزهایی مثل AJAX , DOM tree , setTimeOut و غیره. اینها جز هسته اصلی خود جاوا اسکریپت نیستند، بلکه آبجکت و متدهایی هستند که مرورگر در محیط اجرایی جاوا اسکریپت خودش در اختیار موتور اصلی برنامه میذاره.در واقع موتور جاوا اسکریپت جزیی از محیط اجرایی جاوا اسکریپت مرورگر هست. هر مرورگر موتور مخصوص خودش رو داره، کروم از موتوری به اسم V8 استفاده میکنه که الان میخوایم نگاهی بهش بندازیم.موتور جاوا اسکریپت V8زمانی که کروم کدهای جاوا اسکریپت رو دریافت میکنه، موتور V8 شروع به تجزیه اونها می‌کنه. در ابتدا کل کدها یکبار چک می‌شند تا مشکل و خطای سینتکس نداشته باشند. اگر در این مرحله خطایی مشاهده نشد، موتور شروع به خوندن کدها از بالا به پایین میکنه. هدف نهایی اینه که کدهای جاوا اسکریپت تبدیل به کدهای ماشین بشند تا کامپیوتر بتونه اونها رو متوجه بشه؛ اما قبل از اینکه بخوایم بفهمیم دقیقا چه اتفاقی داره میوفته، باید محیط اجرایی جاوا اسکریپت در مرورگر رو خوب متوجه بشیم. محیط اجرایی جاوا اسکریپت ( Javascript Runtime Environment )این محیط رو مثل یک ظرف بزرگ (دربردارنده) در نظر بگیرید، یک ظرف بزرگی که درونش چندین ظرف کوچکتر و مستقل دیگه وجود داره؛ زمانی که موتور جاوا اسکریپت شروع به تجزیه و تحلیل کدها میکنه، هر بخشی از کد با توجه به عملکردی که داره درونه یکی از این ظرف‌ها قرار میگیره.تصویر ساده برای درک بهتر این محیطپشته‌ی حافظه ( The Heap )اولین دربردارنده (ظرف) در این محیط که جزیی از موتور اصلی جاوا اسکریپت هست، پشته‌ی حافظه ( شما همون Memory Heap بخونید و به ذهن بسپرید ) نام داره. زمانی که موتور به متغیرها و تعریف توابع می‌رسه اونها رو در اینجا ذخیره می‌کنه تا بعدا زمانی که بهشون نیاز داشت ازشون استفاده کنه.پشته‌ی اجرایی ( The Call Stack )دومین دربردارنده‌ در این محیط، Call Stack نام داره، که این هم جزیی از هسته‌ی اصلی موتور جاوا اسکریپت هست. زمانی که موتور به کدهای اکشن محور و اجرایی میرسه اونها رو در اینجا لیست میکنه تا اجراشون کنه.زمانی که یک تابع در استک لیست میشه، جاوا اسکریپت به سرعت شروع به تجزیه کدش میکنه، متغیرهاش رو از حافظه فراخوانی میکنه، یا اگر تابع‌ یا متد دیگه‌ای لازم داشته باشه اون رو به بالای لیست اضافه می‌کنه یا شاید ( با توجه به نوع تابع ) اون رو به ظرف سوم یعنی Web API بفرسته تا مرورگر مسئولیتش رو به عهده بگیره.زمانی که تابع مقداری رو برگردونه یا بسته به شرایطش به ظرف Web API ها ارسال بشه ، فورا از لیست استک حذف می‌شه و تابع بعدی در دستور کار برای تجزیه شدن قرار میگیره. اگر موتور جاوا اسکریپت یک تابع رو کامل حل کنه ولی مقدار مشخصی به شکل صریح ( explicitly ) برگردونده نشده باشه، موتور بطور خودکار مقدار "تعریف نشده ( undefined ) " رو برگشت میده و تابع رو از لیست اجرایی خارج میکنه. این نوع پردازش توابع در جاوا اسکریپت که توابع رو دونه دونه حل میکنه و از لیست اجرایی خودش خارج می‌کنه ( و منتظر مقدار نمیمونه اگر در لحظه درش برگشت داده نشه ) همون چیزی هست که باعث میشه از جاوا اسکریپت به عنوان یک زبان خطی ( synchronous ) نام ببرند. در هر لحظه فقط یک پردازش انجام میده.نکته مهم: ساختار داده در پشته اجرایی جاوا اسکریپت ( Call Stack ) به شکل Last In, First Out هست (LIFO). به غیر از تابعی که در بالای لیست قرار داره، هیچ تابع دیگه‌ای مورد توجه و تحلیل قرار نمیگیره و تا موتور تابع رو حل نکنه ، سراغ تابع بعدی نمیره مگر اینکه یا تابع رو کامل حل کنه یا اون رو به عهده ی مرورگر بذاره. قسمت Web APIزمانی که موتور جاوا اسکریپت به توابع و کدهایی مثل event listeners , HTTP/AJAX request و یا در کل توابعی که در زمان خاصی اعمال میشند میرسه، اونها رو به اینجا میفرسته تا مرورگر مسئولیتش رو به عهده بگیره و در زمان درست، مرورگر اون عمل خاص رو بهش "یادآوری" کنه. این عمل میتونه یک "کلیک" کاربر در جای خاصی از صفحه باشه، یا یک درخواست دریافت اطلاعات از منبع‌ای خارجی. زمانی که هر کدوم از این نوع اعمال اجرا شد، تابع برگشتی اون به لیست انتظار توابع برگشتی ارسال میشه.دلیل این نوع برخورد جاوا اسکریپت اینه که زمان بارگذاری یک صفحه ( از اونجایی که توابع رو یک به یک حل میکنه و خطی پیش میره ) منتظر حل شدن یک تابع نباشه تا مقدارش کامل دستش برسه و بعد بره سراغ تابع بعدی. فرض کنید شما در سایتتون یک درخواست به سایت خارجی دیگه ای فرستادید؛ موقع بارگذاری صفحه اون درخواست ارسال میشه، اگر قرار باشه موتور جاوا اسکریپت تا زمان برگشت مقدار، صفحه رو بارگذاری نکنه، همه چیز با مشکل مواجه میشه. برای همین، موقعی که موتور جاوااسکریپت با چنین توابعی مواجه میشه، درخواست رو ارسال میکنه و تابع رو از لیست اجرایی خودش خارج میکنه ( تا به سراغ تابع بعدی بره ) و مسئولیت اون تابع با مرورگر میمونه تا زمانی که مقداری برگشت داده بشه. صف انتظار Callback هادر این لیست، تمام توابع برگشتی از سمت Web API در صف قرار می‌‎گیرند. نکته‌ی بسیار مهم این هست که این توابع برای اجرا شدن باید تا "خالی شدن" پشته‌ی اجرایی "صبر" کنند. زمانی که پشته‌ی اجرایی کاملا خالی شد، این توابع برای اجرا شدن به اونجا ارسال میشه. زمانی که دوباره استک خالی شد، تابع برگشتی بعدی به اونجا ارسال میشه.نکته مهم: ساختار داده‌ای این قسمت به شکل First In, First Out هست (FIFO). در استک از متد Push , Pop ( اضافه کردن به آخر لیست و برداشتن از اول لیست ) اجرا می‌شد اما در این قسمت متد Push , Shift اجرا میشه ( اضافه کردن به اول لیست و برداشتن از اول لیست )حلقه رویداد ( Event Loop )خب فکر کنم با صحبت‌هایی که تا اینجا شد، خیلی راحت کار حلقه رویداد براتون مشخص بشه. کار این قسمت اینه که به شکل مداوم callback queue و call stack رو چک کنه تا ببینه کی اون لیست خالی میشه یا آیا تابعی در صف قرار گرفته یا نه. شاید در زمانهایی، callback queue و call stack هردو کاملا خالی باشند، ولی Event Loop هرگز غیرفعال نمیشه و دائما در حال چک کردن هردوتاست. زمانی که اولین فانکشن به صف انتظار از سمت مرورگر وارد بشه، EL خیلی سریع Call Stack رو چک میکنه و اگر خالی بود، تابع رو به اونجا ارسال میکنه.زمانی که گفته میشه جاوا اسکریپت به شکل غیرخطی ( asynchronously ) اجرا میشه، منظورشون همینه، در واقع به شکل تکنیکی این حرف غلطه، چون توی واقعیت چنین اتفاقی نمیوفته، ولی اینطور به نظر میاد که انگار جاوا اسکریپت داره چند وظیفه رو به شکل همزمان دنبال میکنه. جاوا اسکریپت در هر لحظه فقط میتونه یک وظیفه رو دنبال کنه و اونم تنها در بالای لیست Stack، اما با کمک Web API هایی که مرورگر در اختیارش قرار میده، میتونه وظایفی که منجر به انتظار هستند رو به مرورگر بسپره و مرورگر در زمان وقوع، اونها رو به صف انتظار بفرسته تا جاوا اسکریپت اونهارو اجرا کنه.مسدود کردن / عدم مسدود کردنوقتی در مورد مسدود کردن صحبت می‌کنیم، به یک حلقه‌ی بینهایت فکر کنید؛ زمانی که یک تابع دائما در حال اجرا باشه. اگر تابع همچنان در حال اجرا باشه هیچوقت از لیست اجرایی خارج نمیشه و در این صورت جلوی اجرا شدن توابع بعدی رو میگیره و یکجورایی اونها رو "مسدود" میکنه. احتمال دیگه‌ای که وجود داره اینه که تابع ما مجبور به انجام محاسبات و منطق پیچیده‌ای باشه که حل شدندش خیلی زمان ببره، طی اون زمان توابع بعدی نمیتونند اجرا بشند و "مسدود" میشند. اینها نکات مهمیه که موقع نوشتن کد برنامه باید در نظر گرفت.نکته‌ی دیگه‌ای که میتونید در نظر داشته باشید، همون مثال HTTP request هست که بالاتر گفتم، اگر قرار باشه بنا به هر دلیلی، جاوا اسکریپت منتظر حل شدن این تابع تا زمان برگشت اطلاعات بمونه، توابع بعدی توی لیست "مسدود" میشند و نمیتونند اجرا بشند. پس همونطور که دیدیم، این کار رو به مرورگر میسپره تا درخواست رو پیگیری کنه.یک مثال ساده برای درک بهتر ..این مثال تو خیلی از ویدیوها و مقاله‌های آموزشی هست، با این مثال شاید بهتر بشه تمام این داستان رو درک کرد...setTimeout(function(){ console.log('Hey, why am I last?'); }, 0); function sayHi(){ console.log('Hello'); } function sayBye(){ console.log('Goodbye'); } sayHi(); sayBye();اگر این کد رو توی کنسول مرورگرتون کپی/پیست کنید، میبینید که به ترتیب مقادیری که دریافت میکنید اول Hello هست، بعد Goodbye ، بعدش undefined و در آخر Hey, why am I last نمایش داده میشه. با اینکه متد setTimeOut در اول فراخوانی شده و زمان تعللش روی 0 ثانیه هست، با اینحال در آخر نمایش داده میشه. کدها رو نگاه کنید و سعی کنید تصور کنید که چه اتفاقی داره پشتش میوفته و موتور جاوا اسکریپت چجوری داره کدها رو تجزیه و تحلیل میکنه. میتونیم باهم بررسی کنیم و ببینیم که موتور V8 گوگل چطور این چندخط کد رو تحلیل میکنه...موتور یکبار کل کدها رو چک می‌کنه تا ببینه خطای سینتکسی وجود داره یا نه. اگر دید که خطایی نیست، شروع به تجزیه کردن کدها میکنه.موتور متد setTimeOut رو میبینه و اون رو توی Call Stack قرار میده.درجا به سراغش میره و میبینه که این متد جزیی از Web API هاست، پس اون رو به ظرف Web API میفرسته و از لیست اجرایی حذفش میکنه.بدلیل اینکه زمان تعلل/تاخیر تابع 0 ثانیست، Web API درلحظه تابع برگشتی رو به صف انتظار میفرسته، Event Loop میبینه که تابعی برای اجرا توی صف قرار گرفته، لیست اجرایی رو چک میکنه تا ببینه اگر خالیه این تابع رو به اونجا منتقل کنه، اما استک خالی نیست چون ....در اولین لحظه‌ای که تابع setTimeOut از استک حذف شد و به Web API فرستاده شد، موتور جاوا اسکریپت تعریف توابع بعدی رو میبینه و اونهارو توی Heap ذخیره میکنه، بعدش فراخوانی تابع sayHi رو میبینه و اون رو به استک برای اجرا شدن میفرسته.تابع sayHi متد console.log رو صدا می‌زنه و اون به بالای لیست اجرا اضافه میشهموتور جاوا اسکریپت سریع سراغ console.log میره و شروع به تجزیه کردنش میکنه و مقدار Hi رو در کنسول چاپ میکنه و متد رو از لیست خارج میکنه.حالا دوباره موتور به تابع sayHi برمیگرده و میبینه که دیگه چیزی برای انجام دادن نیست و به براکت بسته میخوره، پس این تابع رو هم از لیست اجرا خارج میکنه.در لحظه‌ای که این تابع از لیست خارج شد، تابع sayBye به لیست اضافه میشه. تجزیه میشه، متد console.log رو صدا میزنه، در کنسول مقدار Goodbye رو چاپ میکنه، از لیست اجرا خارج میشه، موتور به تابع برمیگرده، اون رو کامل شده میبینه و از لیست اجرا خارج میکنه.حلقه‌ی رویداد ( Event Loop ) بالاخره میبینه که Call Stack خالی شده؛ حالا میتونه اون تابع برگشتی از setTimeOut رو به استک بفرسته تا اجرا بشه. این تابع هم مثل توابع قبلی، متد console.log رو صدا میزنه و روند مشابه به قبلی‌ها رو میره و مقدار Hey, why am I last رو در کنسول چاپ میکنه.شما هم امتحان کنیدبرای اینکه به شکل تصویری بتونید کل این روند رو ببینید، میتونید از این وبسایت استفاده کنید. در این وبسایت به شکل کامل تمام اتفاق‌های درون محیط اجرایی جاوا اسکریپت مرورگر با سرعت آهسته و قدم به قدم اجرا میشه که شما میتونید خیلی راحت هر قدم رو دنبال کنید.امیدوارم از این مطلب لذت برده باشید. سعی میکنم از این به بعد مقاله‌‎هایی که میخونم و فکر میکنم می‌تونند مفید باشند رو اینجا به شکل ترجمه/تالیف بذارم.