در این مقاله نگاهی به محیط اجرایی جاوا اسکریپت ( ترجمهی سخت Runtime environment) در بستر مرورگر میندازیم و یاد میگیریم که موتور جاوا اسکریپتِ V8 گوگل، چطور کدها رو تحلیل و تجزیه ( Parse ) میکنه و همچنین با نقش حلقه ی رویداد ( Event Loop ) در شرایط Single Thread ، چه به شکل خطی ( synchronously ) و "یک جورایی" غیر خطی ( asynchronously ) آشنا میشیم.
هر مرورگر ( مثل کروم، فایرفاکس یا سافاری ) یک محیط اجرایی جاوا اسکریپت داره که یکسری API ها رو در اختیار توسعه دهنده/برنامه نویس میذاره، چیزهایی مثل AJAX , DOM tree , setTimeOut و غیره. اینها جز هسته اصلی خود جاوا اسکریپت نیستند، بلکه آبجکت و متدهایی هستند که مرورگر در محیط اجرایی جاوا اسکریپت خودش در اختیار موتور اصلی برنامه میذاره.
در واقع موتور جاوا اسکریپت جزیی از محیط اجرایی جاوا اسکریپت مرورگر هست. هر مرورگر موتور مخصوص خودش رو داره، کروم از موتوری به اسم V8 استفاده میکنه که الان میخوایم نگاهی بهش بندازیم.
زمانی که کروم کدهای جاوا اسکریپت رو دریافت میکنه، موتور V8 شروع به تجزیه اونها میکنه. در ابتدا کل کدها یکبار چک میشند تا مشکل و خطای سینتکس نداشته باشند. اگر در این مرحله خطایی مشاهده نشد، موتور شروع به خوندن کدها از بالا به پایین میکنه. هدف نهایی اینه که کدهای جاوا اسکریپت تبدیل به کدهای ماشین بشند تا کامپیوتر بتونه اونها رو متوجه بشه؛ اما قبل از اینکه بخوایم بفهمیم دقیقا چه اتفاقی داره میوفته، باید محیط اجرایی جاوا اسکریپت در مرورگر رو خوب متوجه بشیم.
این محیط رو مثل یک ظرف بزرگ (دربردارنده) در نظر بگیرید، یک ظرف بزرگی که درونش چندین ظرف کوچکتر و مستقل دیگه وجود داره؛ زمانی که موتور جاوا اسکریپت شروع به تجزیه و تحلیل کدها میکنه، هر بخشی از کد با توجه به عملکردی که داره درونه یکی از این ظرفها قرار میگیره.
اولین دربردارنده (ظرف) در این محیط که جزیی از موتور اصلی جاوا اسکریپت هست، پشتهی حافظه ( شما همون Memory Heap بخونید و به ذهن بسپرید ) نام داره. زمانی که موتور به متغیرها و تعریف توابع میرسه اونها رو در اینجا ذخیره میکنه تا بعدا زمانی که بهشون نیاز داشت ازشون استفاده کنه.
دومین دربردارنده در این محیط، Call Stack نام داره، که این هم جزیی از هستهی اصلی موتور جاوا اسکریپت هست. زمانی که موتور به کدهای اکشن محور و اجرایی میرسه اونها رو در اینجا لیست میکنه تا اجراشون کنه.
زمانی که یک تابع در استک لیست میشه، جاوا اسکریپت به سرعت شروع به تجزیه کدش میکنه، متغیرهاش رو از حافظه فراخوانی میکنه، یا اگر تابع یا متد دیگهای لازم داشته باشه اون رو به بالای لیست اضافه میکنه یا شاید ( با توجه به نوع تابع ) اون رو به ظرف سوم یعنی Web API بفرسته تا مرورگر مسئولیتش رو به عهده بگیره.
زمانی که تابع مقداری رو برگردونه یا بسته به شرایطش به ظرف Web API ها ارسال بشه ، فورا از لیست استک حذف میشه و تابع بعدی در دستور کار برای تجزیه شدن قرار میگیره.
اگر موتور جاوا اسکریپت یک تابع رو کامل حل کنه ولی مقدار مشخصی به شکل صریح ( explicitly ) برگردونده نشده باشه، موتور بطور خودکار مقدار "تعریف نشده ( undefined ) " رو برگشت میده و تابع رو از لیست اجرایی خارج میکنه.
این نوع پردازش توابع در جاوا اسکریپت که توابع رو دونه دونه حل میکنه و از لیست اجرایی خودش خارج میکنه ( و منتظر مقدار نمیمونه اگر در لحظه درش برگشت داده نشه ) همون چیزی هست که باعث میشه از جاوا اسکریپت به عنوان یک زبان خطی ( synchronous ) نام ببرند. در هر لحظه فقط یک پردازش انجام میده.
نکته مهم: ساختار داده در پشته اجرایی جاوا اسکریپت ( Call Stack ) به شکل Last In, First Out هست (LIFO). به غیر از تابعی که در بالای لیست قرار داره، هیچ تابع دیگهای مورد توجه و تحلیل قرار نمیگیره و تا موتور تابع رو حل نکنه ، سراغ تابع بعدی نمیره مگر اینکه یا تابع رو کامل حل کنه یا اون رو به عهده ی مرورگر بذاره.
زمانی که موتور جاوا اسکریپت به توابع و کدهایی مثل event listeners , HTTP/AJAX request و یا در کل توابعی که در زمان خاصی اعمال میشند میرسه، اونها رو به اینجا میفرسته تا مرورگر مسئولیتش رو به عهده بگیره و در زمان درست، مرورگر اون عمل خاص رو بهش "یادآوری" کنه. این عمل میتونه یک "کلیک" کاربر در جای خاصی از صفحه باشه، یا یک درخواست دریافت اطلاعات از منبعای خارجی. زمانی که هر کدوم از این نوع اعمال اجرا شد، تابع برگشتی اون به لیست انتظار توابع برگشتی ارسال میشه.
دلیل این نوع برخورد جاوا اسکریپت اینه که زمان بارگذاری یک صفحه ( از اونجایی که توابع رو یک به یک حل میکنه و خطی پیش میره ) منتظر حل شدن یک تابع نباشه تا مقدارش کامل دستش برسه و بعد بره سراغ تابع بعدی. فرض کنید شما در سایتتون یک درخواست به سایت خارجی دیگه ای فرستادید؛ موقع بارگذاری صفحه اون درخواست ارسال میشه، اگر قرار باشه موتور جاوا اسکریپت تا زمان برگشت مقدار، صفحه رو بارگذاری نکنه، همه چیز با مشکل مواجه میشه. برای همین، موقعی که موتور جاوااسکریپت با چنین توابعی مواجه میشه، درخواست رو ارسال میکنه و تابع رو از لیست اجرایی خودش خارج میکنه ( تا به سراغ تابع بعدی بره ) و مسئولیت اون تابع با مرورگر میمونه تا زمانی که مقداری برگشت داده بشه.
در این لیست، تمام توابع برگشتی از سمت Web API در صف قرار میگیرند. نکتهی بسیار مهم این هست که این توابع برای اجرا شدن باید تا "خالی شدن" پشتهی اجرایی "صبر" کنند. زمانی که پشتهی اجرایی کاملا خالی شد، این توابع برای اجرا شدن به اونجا ارسال میشه. زمانی که دوباره استک خالی شد، تابع برگشتی بعدی به اونجا ارسال میشه.
نکته مهم: ساختار دادهای این قسمت به شکل First In, First Out هست (FIFO). در استک از متد Push , Pop ( اضافه کردن به آخر لیست و برداشتن از اول لیست ) اجرا میشد اما در این قسمت متد Push , Shift اجرا میشه ( اضافه کردن به اول لیست و برداشتن از اول لیست )
خب فکر کنم با صحبتهایی که تا اینجا شد، خیلی راحت کار حلقه رویداد براتون مشخص بشه. کار این قسمت اینه که به شکل مداوم 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 گوگل چطور این چندخط کد رو تحلیل میکنه...
برای اینکه به شکل تصویری بتونید کل این روند رو ببینید، میتونید از این وبسایت استفاده کنید. در این وبسایت به شکل کامل تمام اتفاقهای درون محیط اجرایی جاوا اسکریپت مرورگر با سرعت آهسته و قدم به قدم اجرا میشه که شما میتونید خیلی راحت هر قدم رو دنبال کنید.
امیدوارم از این مطلب لذت برده باشید. سعی میکنم از این به بعد مقالههایی که میخونم و فکر میکنم میتونند مفید باشند رو اینجا به شکل ترجمه/تالیف بذارم.