Event Loop در JavaScript

Event loop در Javascript و پیدایش رفتارهای ناهمزمان


Event Loop یکی از جنبه های خیلی مهم برای درک و فهمیدن JavaScript محسوب میشود. در این پست به توضیح event loop  می پردازیم:

مقدمه

Event Loop از مهمترین جنبه هایی است که برای فهمیدن JavaScript به درک آن نیاز داریم.

من چند سالی هست که با JavaScript برنامه نویسی میکنم ولی هنوز به طور کامل نحوه کارکردش رو درک نکردم، بنابراین خیلی طبیعی هستش که این مفهوم رو با جزئیات ندونید اما طبق معمول دونستن نحوه کارکرد event loop بسیار مفید است.

این مقاله با هدف تشریح جزئیات درونی کارکرد JavaScript با یک thread و نحوه مدیریت توابع asynchronous می باشد.

کد JavaScript ما به صورت single thread اجرا می شود یعنی در هر لحظه فقط یک رویداد در حال انجام است.

این محدودیت در واقع خیلی مفید است، به این دلیل که برنامه‌نویسی شما رو بدون در نظر گرفتن مسائل concurrency راحت می کند.

شما فقط باید به نحوه نوشتن کد خودتون توجه کنید طوری که از هر چیزی که thread رو block کنه، مثل حلقه ی بینهایت یا call کردن های همزمان network بپرهیزید.

به طور کلی، در اکثر مرورگرها یک event loop برای هر browser tab وجود دارد تا فرآیندها را جدا کرده و از مسدود شدن کل browser شما به خاطر یک صفحه وب با حلقه های بی نهایت یا پردازش زیاد جلوگیری کند.

environment چندین حلقه رویداد همزمان را به عنوان مثال برای مدیریت تماس های API مدیریت می کند. Web Workers نیز در حلقه رویداد خود اجرا می شوند.

شما عمدتا باید متوجه این نکته باشید که کد شما در یک event loop اجرا می شود و برای جلوگیری از block شدن آن، کدتان را با توجه به این نکته بنویسید.

Block شدن event loop

هر کد جاوا اسکریپتی که مدت زمان طولانی بگیرد تا کنترل به event loop برگردد، از اجرای هر کد جاوا اسکریپت در صفحه جلوگیری می کند، حتی thread مربوط به UI را مسدود می کند و کاربر نمی تواند کلیک کند یا صفحه را پیمایش کند و ....

تقریبا تمامی I/O های اولیه در JavaScript، رفتار non-blocking دارند. مانند درخواست های network، عملیات فایل سیستم در Node.js و غیره. block کردن یک exception است و به همین دلیل JavaScript اساساً بر اساس Callbackها، و اخیراً بر اساس promiseها و async/await استوار است.

call stack ها

call stack یک صف LIFO است (Last In، First Out).

event loop به طور مداوم call stack را بررسی می کند تا ببیند آیا function ی لازم است که اجرا شود.

در حالی که این کار را انجام می دهد، هر فراخوانی تابعی را که پیدا می کند به call stack اضافه می کند و هر کدام را به ترتیب اجرا می کند.

احتمالا با error stack trace آشنا هستید، آیا می دانید که error stack trace در debugger اجرا می شود یا در browser console؟ 

browser نام تابع را در call stack جستجو می کند تا به شما اطلاع دهد که کدام تابع موجب این فراخوانی بوده است:

Exception call stack

یک توضیح ساده event loop

بگذارید یک مثال بزنم:

من از foo, bar و  baz  به عنوان random names استفاده کردم.  هر نامگذاری دلخواهی جای آنها می توانید بگذارید.

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()

خروجی کد:

foo
bar
baz

همانطور که انتظار می رفت.

وقتی کد اجرا می شود، اول foo() می شود، داخل foo()  ما ابتدا bar() را فراخوانی می کنیم، سپس baz().

در اینجا، call stack به شکل زیر می شود:

Call stack first example

 

event loop در هر تکرار نگاه می کند که اگر در call stack چیزی باقی مانده باشد، آنرا اجرا می کند:Execution order first example

تا زمانی که call stack خالی شود.

اجرای توابع در Queue

مثال بالا به نظر ساده می آمد و چیز خاصی درباره آن وجود نداشت. JavaScript چیزها را برای اجرا پیدا می کند، سپس به ترتیب اجرا می کند.

بیایید ببینیم چگونه اجرای function را به تعویق بیندازیم تا stack خالی شود.

قطعه کد  setTimeout(() => {}), 0) یک فراخوانی تابع است، اما زمانی که یک تابع دیگر در کد اجرا شد، آن را اجرا کنید.

این مثال را ببینید:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()

خروجی کد که البته شاید عجیب باشد:

foo
baz
bar

وقتی کد اجرا می شود ابتدا foo() فراخوانی می شود، داخل foo() ما ابتدا setTimeout را فراخوانی می کنیم و bar را به عنوان آرگومان به آن می دهیم، و به آن دستور می دهیم که بلافاصله اجرا شود و ۰ را به عنوان تایمر به آن پاس می دهیم، سپس baz() را فراخوانی می کنیم.

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

Call stack second example

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

Execution order second example

چرا این اتفاق افتاد؟

Message Queue

با فراخوانی setTimeout() ، مرورگر یا Node.js تایمر را شروع می کنند. پس از انقضای تایمر، در این حالت بلافاصله همانطور که 0 را به عنوان مهلت زمانی قرار می دهیم، عملکرد برگشت در message queue قرار می گیرد.

Message Queue نیز جایی است که رویدادهای آغاز شده توسط کاربر مانند رویدادهای کلیک یا صفحه کلید یا پاسخ های fetch قبل از اینکه کد شما فرصت واکنش به آنها را داشته باشد، در صف قرار می گیرند. یا همچنین رویدادهای DOM مانند onLoad.

event loop اولویت را به call stack می دهد، و در ابتدا هر آنچه که در آن پیدا کند، اجرا می کند، سپس پس از آنکه call stack خالی شد، سراغ message queue می رود.

برای انجام کارهای خود لازم نیست منتظر توابعی مانند setTimeout ، fetch یا موارد دیگر باشیم، زیرا آنها توسط مرورگر ارائه می شوند و در thread های خاص خود زندگی می کنند. به عنوان مثال، اگر مهلت زمانی setTimeout را روی 2 ثانیه تنظیم کنید، لازم نیست 2 ثانیه صبر کنید - انتظار در جای دیگری اتفاق می افتد.

Job Queue در ES6

ECMAScript 2015 مفهوم Job Queue را که توسط Promises استفاده می شود (در ES6/ES2015 نیز معرفی شده است) معرفی کرد. این روشی است که به جای اینکه در پشت call stack قرار بگیرد، نتیجه عملکرد تابع async را در اسرع وقت انجام دهد.

Promise هایی که قبل از اینکه function فعلی پایان پذیرد، Resolve می شوند، درست بعد از اجرای function فعلی، اجرا می شوند.

به نظر من مثال شهربازی خوب باشد:

Queue Job شما را در پشت صف قرار می دهد، پشت سر همه افراد دیگ ، جایی که باید منتظر نوبت خود باشید، در حالی که صف بلیط سریع گذر است که به شما امکان می دهد درست بعد از اتمام بلیط قبلی، مجددا سوار دیگری شوید.

مثال:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('should be right after baz, before bar')
  ).then(resolve => console.log(resolve))
  baz()
}

foo()

خروجی:

foo
baz
should be right after baz, before bar
bar

این یک تفاوت بزرگ بین Promiseها (و Async/Await است که براساس Promise ها ساخته شده است) و توابع asynchronous ساده قدیمی از طریق setTimeout() یا سایر Platform API ها است.

سعید نصیری
سعید نصیری

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

× در حال پاسخ به: