Anophel-آنوفل جاوااسکریپت چگونه کار می کند؟ بررسی Event Loop و Call Stack

جاوااسکریپت چگونه کار می کند؟ بررسی Event Loop و Call Stack

انتشار:
2

هنگام کار با جاوااسکریپت شاید برای شما هم سوال باشد که جاوااسکریپت چگونه کار می کند؟ در این مقاله، نحوه کار Call Stack و Event Loop، صف جاب ها و موارد دیگر را توضیح می دهیم.

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

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

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

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

توجه داشته باشید که چگونه بسیاری از چیزهای گرافیکی بخشی از خود زبان جاوا اسکریپت نیستند. APIهای وب، صف کال بک ها و Event Loop همگی ویژگی هایی هستند که مرورگر ارائه می کند.

نمایش NodeJS مشابه به نظر می رسد، اما در این مقاله، من بر نحوه عملکرد جاوا اسکریپت در مرورگر تمرکز خواهم کرد.

برای بررسی این که چرا جاوااسکریپت گزینه خوبی برای مبتدیان است؟ این مقاله را بررسی کنید.

Call Stack چیست؟

احتمالاً قبلاً شنیده اید که جاوا اسکریپت single-threaded است. اما به چه معنی است؟ جاوا اسکریپت می تواند یک کار را در یک زمان انجام دهد زیرا فقط یک Call Stack دارد.

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

هر بار که یک اسکریپت یا تابع یک تابع را فراخوانی می کند، به بالای Call Stack اضافه می شود. هر بار که تابع خارج می شود، مفسر آن را از Call Stack حذف می کند.

یک تابع یا از طریق عبارت return یا با رسیدن به انتهای محدوده خارج می شود.

کد زیر را در نظر بگرید و آن را اجرا کنید : 

const addOne = (value) => value + 1;
const addTwo = (value) => addOne(value + 1);
const addThree = (value) => addTwo(value + 1);
const calculation = () => {
  return addThree(1) + addTwo(2);
};

calculation();

هر بار که یک تابع تابع دیگری را فراخوانی می کند، به بالای استک، بالای تابع فراخوانی اضافه می شود. ترتیبی که استک هر فراخوانی تابع را پردازش می کند از اصل LIFO (آخرین ورود، اولین خروج) پیروی می کند.

مراحل مثال قبلی به شرح زیر است:

1. فایل بارگیری می شود و تابع اصلی فراخوانی می شود که مخفف اجرای کل فایل است. این تابع به Call Stack اضافه می شود.
2. main صدا می زند ()calculation، به همین دلیل است که به بالای Call Stack اضافه می شود.
3. ()calculation نیز  ()adThree را فرا می خواند که دوباره به Call Stack اضافه می شود.
4. addThree نیز  addTwo را فرا می خواند که به Call Stack اضافه می شود.
...

6. addOne هیچ تابع دیگری را فراخوانی نمی کند. وقتی خارج می شود، از Call Stack حذف می شود.
7. با نتیجه addOne، addTwo نیز خارج می شود و از Call Stack حذف می شود.
8. addThree نیز در حال حذف است.
9. محاسبه addTwo را فراخوانی می کند که آن را به Call Stack اضافه می کند.
10. addTwo با addOne تماس گرفته و آن را به Call Stack اضافه می کند.
11. addOne خارج می شود و از Call Stack حذف می شود.
12. addTwo خارج می شود و از Call Stack حذف می شود.
13.محاسبه اکنون می تواند با نتیجه addThree و addTwo خارج شود و از Call Stack حذف می شود.
14. هیچ دستور یا فراخوانی دیگری در فایل وجود ندارد، بنابراین main نیز خارج می شود و از Call Stack حذف می شود.

دقت کنید ما context را که کد ما را اجرا می کند main نامیدم، اما نام رسمی تابع اینگونه نیست. در پیام های خطایی که در کنسول مرورگر می توانید پیدا کنید، نام این تابع ناشناس است.

Uncaught RangeError: Maximum call stack size exceeded

دنباله ردپای این پیغام خطا را دنبال کنید. این نشان دهنده فراخوانی توابعی است که منجر به این خطا شده است. در این حالت، خطا در تابع b بود که توسط a فراخوانی شده است (که با b و غیره فراخوانی شده است).

اگر این پیغام خطای خاص را روی صفحه نمایش خود مشاهده کردید، یکی از توابع شما تعداد زیادی توابع را فراخوانی کرده است. حداکثر اندازه Call Stack بین 10 تا 50 هزار فرا خوانی است، بنابراین اگر از آن فراتر رفتید، به احتمال زیاد یک حلقه نامحدود در کد خود دارید.

مرورگر با محدود کردن Call Stack از مسدود کردن کد شما در کل صفحه جلوگیری می کند. با کد زیر دوباره خطا را ایجاد کردم. یک راه برای جلوگیری از این امر، استفاده نکردن از توابع بازگشتی در وهله اول یا با ارائه یک حالت پایه است که باعث می شود تابع شما در نقطه ی خارج شود.

function a() {
    b();
}

function b() {
    a();
}

a();

به طور خلاصه، Call Stack، فراخوانی های تابع را در کد شما پیگیری می کند. از اصل LIFO (آخرین ورود، اولین خروج) پیروی می کند، به این معنی که همیشه ابتدا فرا خوانی را که بالای استک است پردازش می کند.

جاوا اسکریپت فقط یک Call Stack دارد، به همین دلیل است که فقط یک کار را در یک زمان انجام می دهد.

Heap چیست؟

Heap در جاوا اسکریپت جایی است که وقتی توابع یا متغیرها را تعریف می کنیم، آبجکت ها ذخیره می شوند.

از آنجایی که بر Call Stack و Event Loop تأثیر نمی گذارد، توضیح نحوه عملکرد تخصیص حافظه جاوا اسکریپت از این مقاله خارج است.

اگر می خواهید در مورد این موضوع بیشتر بدانید، بزودی مقاله ای را در این خصوص منتشر خواهیم کرد و لینک آن را نیز در این مقاله قرار خواهیم داد.

API های وب

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

برای مثال، بیایید نگاهی به نحوه ایجاد درخواست API بیندازیم. اگر کد را در مفسر جاوا اسکریپت اجرا کنیم، تا زمانی که پاسخی از سرور دریافت نکنیم، نمی‌توانیم کار دیگری انجام دهیم. تا حد زیادی برنامه های وب را غیر قابل استفاده می کند.

به عنوان راه حلی برای این، مرورگرهای وب به ما API هایی می دهند که می توانیم آنها را در کد جاوا اسکریپت خود فراخوانی کنیم. با این حال، اجرا توسط خود پلتفرم انجام می شود، به همین دلیل است که Call Stack را مسدود نمی کند.

مزیت دیگر APIهای وب این است که با کدهای سطح پایین (مانند C) نوشته می شوند، که به آنها اجازه می دهد کارهایی را انجام دهند که در جاوا اسکریپت ساده امکان پذیر نیست.

آنها شما را قادر می سازند درخواست های AJAX یا DOM را دستکاری کنید، اما همچنین طیف وسیعی از چیزهای دیگر مانند ردیابی جغرافیایی، دسترسی به فضای ذخیره سازی محلی، service workers و موارد دیگر را نیز ممکن می سازد.

صف Callback چیست؟

با ویژگی های وب API، اکنون می توانیم کارهایی را به طور همزمان خارج از مفسر جاوا اسکریپت انجام دهیم. اما چه اتفاقی می افتد اگر بخواهیم کد جاوا اسکریپت ما به نتیجه یک Web API واکنش نشان دهد، مثلاً یک درخواست AJAX؟

اینجاست که callback ها وارد عمل می شوند. از طریق آنها، وب APIها به ما اجازه می دهند تا پس از پایان اجرای فراخوانی API، کد را اجرا کنیم.

کال بک (callback) چیست؟

callback تابعی است که به عنوان آرگومان به تابع دیگری ارسال می شود. callback معمولاً پس از اتمام کد اجرا می شود.

شما می توانید با نوشتن توابعی که تابعی را به عنوان آرگومان می پذیرند، توابع callback ایجاد کنید. چنین توابعی به عنوان توابع مرتبه بالاتر نیز شناخته می شوند. توجه داشته باشید که callback ها به طور پیش فرض ناهمزمان نیستند.

بیایید به یک مثال نگاه کنیم:

const a = () => console.log('a');
const b = () => setTimeout(() => console.log('b'), 100);
const c = () => console.log('c');

a();
b();
c();

احتمالاً می توانید از قبل به این فکر کنید که خروجی چگونه خواهد بود.

setTimeout به طور همزمان اجرا می شود در حالی که مفسر JS به اجرای دستورات بعدی ادامه می دهد. هنگامی که مهلت زمانی تمام شد و Call Stack مجدداً خالی شد، تابع callback که به setTimeout ارسال شده است اجرا خواهد شد.

خروجی نهایی به شکل زیر خواهد بود:

a
c
b

اما در مورد صف callback چطور؟
اکنون، پس از اتمام اجرای setTimeout، فوراً تابع callback را فراخوانی نمی‌کند. اما چرا اینطور است؟

به یاد داشته باشید که جاوا اسکریپت فقط می تواند یک کار را در یک زمان انجام دهد؟

callback که به عنوان آرگومان به setTimeout ارسال کردیم در جاوا اسکریپت نوشته شده است. بنابراین، مفسر جاوا اسکریپت باید کد را اجرا کند، به این معنی که باید از Call Stack استفاده کند، که دوباره به این معنی است که باید منتظر بمانیم تا استک فراخوانی خالی شود تا بتوانیم فراخوانی را اجرا کنیم.

فراخوانی setTimeout اجرای Web API را آغاز می کند، که callback را به صف callback اضافه می کند. Event Loop سپس callback را از صف می گیرد و به محض خالی شدن آن را به استک اضافه می کند.

برخلاف Call Stack، صف callback از ترتیب FIFO (اول وارد، اول خارج) پیروی می کند، به این معنی که callback ها به همان ترتیبی که به صف اضافه شده اند پردازش می شوند.

Event Loop چیست؟

Event Loop جاوا اسکریپت اولین فرا خوانی را در صف callback می گیرد و به محض خالی شدن آن را به Call Stack اضافه می کند.

کد جاوا اسکریپت به صورت اجرا تا تکمیل اجرا می‌شود، به این معنی که اگر Call Stack در حال اجرای برخی از کدها باشد، Event Loop مسدود می‌شود و تا زمانی که استک دوباره خالی نشود، هیچ فرا خوانی از صف اضافه نخواهد شد.

به همین دلیل مهم است که Call Stack را با اجرای وظایف محاسباتی مسدود نکنید.

اگر کد زیادی را اجرا کنید یا صف callback خود را مسدود کنید، وب سایت شما پاسخگو نخواهد بود زیرا نمی تواند کد جاوا اسکریپت جدیدی را اجرا کند.

کنترل‌کننده‌های رویداد، مانند onscroll، وقتی فعال می‌شوند، وظایف بیشتری را به صف callback اضافه می‌کنند. به همین دلیل است که شما باید این callback ها را حذف کنید، به این معنی که آنها فقط در هر x ms اجرا می شوند.

خودتان ببینید
کد زیر را به کنسول مرورگر خود اضافه کنید. همانطور که پیمایش می کنید، می توانید مشاهده کنید که چند وقت یکبار اسکرول کردن callback چاپ scroll صورت می گیرد.

window.onscroll = () => console.log('scroll');

setTimeout (fn, 0)

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

قرار دادن کد ناهمزمان خود در یک callback و تنظیم setTimeout روی 0ms به مرورگر این امکان را می‌دهد که کارهایی مانند به‌روزرسانی DOM را قبل از ادامه اجرای کال بک انجام دهد.

صف جاب و کد ناهمزمان (asynchronous) چیست؟

در نمای کلی که در ابتدا نشان دادم، یک ویژگی اضافی را که دانستن آن مهم است کنار گذاشتم.

علاوه بر صف کال بک، صف دیگری وجود دارد که به طور انحصاری پرومیس ها را می‌پذیرد، صف جاب (job queue) است.

پرومیس ها در جاوااسکریپت: یک جمع بندی سریع

EcmaScript 2015 (یا ES6) اولین بار پرومیس ها را معرفی کرد، حتی اگر قبلاً در Babel در دسترس بوده است. اگر هنوز تفاوت بین اکما اسکریپت را با جاوااسکریپت را درک نکرده اید این مقاله می تواند در درک آن به شما کمک کند.

Promise ها روش دیگری برای مدیریت کدهای ناهمزمان به غیر از استفاده از callbacks است. آنها به شما این امکان را می دهند که به راحتی توابع ناهمزمان را بدون اینکه به چیزی که به آن جهنم کال بک ها یا هرم عذاب گفته می شود ختم شوید.

setTimeout(() => {
  console.log('Print this and wait');
  setTimeout(() => {
    console.log('Do something else and wait');
    setTimeout(() => {
      // ...
    }, 100);
  }, 100);
}, 100)

با پرومیس‌ها، این کد می‌تواند بسیار خواناتر شود:

// A promise wrapper for setTimeout
const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));
timeout(1000)
  .then(() => {
    console.log('Hi after 1 second');
    return timeout(1000);
  })
  .then(() => {
    console.log('Hi after 2 seconds');
  });

این کد با دستور async/wait خواناتر به نظر می رسد:

const logDelayedMessages = async () => {
  await timeout(1000);
  console.log('Hi after 1 second');
  await timeout(1000);
  console.log('Hi after 2 seconds');
};

logDelayedMessages();

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

پرومیس ها در کجا جای می گیرند؟

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

صف جاب که به صف پرومیس نیز معروف است، مانند صف های سریع در یک شهربازی، نسبت به صف کال بک ها اولویت دارد. Event Loop قبل از پردازش صف کال بک ها را از صف پرومیس می گیرد.

بیایید به یک مثال نگاه کنیم:

console.log('a');
setTimeout(() => console.log('b'), 0);
new Promise((resolve, reject) => {
  resolve();
})
.then(() => {
  console.log('c');
});
console.log('d');

با در نظر گرفتن دانش شما در مورد نحوه عملکرد صف های کال بک، ممکن است فکر کنید که خروجی a d b c خواهد بود.

اما از آنجایی که صف پرومیس نسبت به صف کال بک ها اولویت دارد، c قبل از b چاپ می شود، حتی اگر هر دو ناهمزمان باشند:

a
d
c
b

نتیجه

امیدوارم اکنون متوجه شده باشید که در پشت صحنه کد جاوا اسکریپت چه اتفاقی می افتد. با پرومیس ها و event loop ها  و کال استک ها آشنا شدیم و نسبت به جاوا اسکریپت به درک عمیق تری دست یافتیم. همچنین برای آشنایی با نحوه دیباگ کردن در جاوااسکریپت این مقاله را بررسی کنید.

#جاوااسکریپت#js#event_loops#callback#call_stack#job_queue#promises#javascript
نظرات ارزشمند شما :

در حال دریافت...

مقاله های مشابه

در حال دریافت...