۱۱ سپتامبر ۲۰۲۳

Event loop: microtasks و macrotasks

اجرای Browser JavaScript execution flow، و همچنین Node.js، بر اساس یک event loop است.

درک نحوه عملکرد حلقه رویداد برای بهینه سازی ها و گاهی اوقات برای معماری مناسب مهم است.

در این فصل ابتدا جزئیات نظری در مورد چگونگی کارکرد چیزها را پوشش می دهیم و سپس کاربردهای عملی آن دانش را مشاهده می کنیم.

Event Loop

مفهوم event loop بسیار ساده است. یک حلقه بی پایان وجود دارد که در آن موتور جاوااسکریپت منتظر وظایف می ماند، آنها را اجرا می کند و سپس به خواب می رود و منتظر کارهای بیشتر است.

الگوریتم کلی موتور:

  1. در حالی که وظایف وجود دارد:
    • آنها را با قدیمی ترین کار شروع کنید.
  2. بخوابید تا زمانی که یک کار ظاهر شود، سپس به 1 بروید.

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

نمونه هایی از وظایف:

  • وقتی یک اسکریپت خارجی <script src="..."> بارگیری می شود، وظیفه اجرای آن است.
  • هنگامی که یک کاربر ماوس خود را حرکت می دهد، وظیفه ارسال رویداد mousemove و اجرای کنترل کننده ها است.
  • هنگامی که زمان تعیین شده برای setTimeout برنامه ریزی شده است، کار این است که تماس مجدد آن را اجرا کنید.
  • … و غیره.

وظایف تنظیم می شوند – موتور آنها را مدیریت می کند – سپس منتظر کارهای بیشتری می ماند (در حالت خواب و مصرف CPU نزدیک به صفر).

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

وظایف یک صف تشکیل می دهند که اصطلاحاً به آن “macrotask queue” (v8 term) می گویند:

به عنوان مثال، در حالی که موتور مشغول اجرای یک script است، یک کاربر ممکن است ماوس خود را حرکت دهد و باعث mousemove شود، و setTimeout ممکن است به دلیل وجود داشته باشد و غیره، این وظایف یک صف تشکیل می دهند، همانطور که در تصویر بالا نشان داده شده است.

وظایف از صف بر اساس “first come – first served” پردازش می شود. وقتی مرورگر موتور با script تمام شد، رویداد mousemove و سپس setTimeoutو غیره را کنترل می‌کند.

تا اینجا، کاملا ساده، درست است؟

دو جزئیات دیگر:

  1. رندر هرگز اتفاق نمی افتد در حالی که موتور یک کار را اجرا می کند. مهم نیست که کار زمان زیادی ببرد. تغییرات در DOM فقط پس از تکمیل کار انجام می شود.
  2. اگر یک کار بیش از حد طولانی شود، مرورگر نمی تواند کارهای دیگری مانند پردازش رویدادهای کاربر را انجام دهد. بنابراین پس از مدتی، هشداری مانند “Page Unresponsive” را مطرح می‌کند که نشان می‌دهد کار با کل صفحه از بین می‌رود. این زمانی اتفاق می افتد که محاسبات پیچیده زیادی وجود داشته باشد یا یک خطای برنامه نویسی منجر به یک حلقه بی نهایت شود.

این نظریه بود. حال بیایید ببینیم چگونه می توانیم این دانش را به کار ببریم.

استفاده ۱: تقسیم کردن تسک های CPU-hungry

بیایید بگوییم که ما یک وظیفه تشنه CPU داریم.

برای مثال، برجسته کردن نحو (که برای رنگ آمیزی نمونه های کد در این صفحه استفاده می شود) کاملاً از نظر CPU سنگین است. برای برجسته کردن کد، تجزیه و تحلیل را انجام می دهد، عناصر رنگی زیادی ایجاد می کند، آنها را به سند اضافه می کند – برای مقدار زیادی متن که زمان زیادی می برد.

در حالی که موتور مشغول برجسته‌سازی نحو است، نمی‌تواند سایر کارهای مربوط به DOM، پردازش رویدادهای کاربر و غیره را انجام دهد. حتی ممکن است باعث شود مرورگر برای مدتی “hiccup” یا حتی “hang” کند، که غیرقابل قبول است.

ما می توانیم با تقسیم کار بزرگ به قطعات از مشکلات جلوگیری کنیم. ابتدا 100 خط را برجسته کنید، سپس setTimeout (با تاخیر صفر) را برای 100 خط بعدی برنامه ریزی کنید، و به همین ترتیب.

برای نشان دادن این رویکرد، به‌خاطر سادگی، به‌جای برجسته‌سازی متن، تابعی را انتخاب می‌کنیم که از 1 تا 1000000000 محاسبه می‌شود.

اگر کد زیر را اجرا کنید، موتور برای مدتی “hang” خواهد شد. برای JS سمت سرور که به وضوح قابل توجه است، و اگر آن را در مرورگر اجرا می کنید، سپس سعی کنید روی دکمه های دیگر صفحه کلیک کنید – خواهید دید که تا زمانی که شمارش به پایان برسد هیچ رویداد دیگری مدیریت نمی شود.

let i = 0;

let start = Date.now();

function count() {

  // do a heavy job
  for (let j = 0; j < 1e9; j++) {
    i++;
  }

  alert("Done in " + (Date.now() - start) + 'ms');
}

count();

حتی ممکن است مرورگر اخطار «اسکریپت خیلی طولانی است» را نشان دهد.

بیایید کار را با استفاده از تماس‌های setTimeout تودرتو تقسیم کنیم:

let i = 0;

let start = Date.now();

function count() {

  // do a piece of the heavy job (*)
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // schedule the new call (**)
  }

}

count();

اکنون رابط مرورگر در طول فرآیند “شمارش” کاملاً کاربردی است.

یک اجرا از «شمارش» بخشی از کار (*) را انجام می دهد، و سپس در صورت نیاز، (**) را دوباره برنامه ریزی می کند:

  1. تعداد اجرای اول: i=1...1000000.
  2. تعداد اجرای دوم: i=1000001..2000000.
  3. … و غیره.

اکنون، اگر یک کار جانبی جدید (مثلاً رویداد onclick) در حالی که موتور مشغول اجرای قسمت 1 است ظاهر شود، در صف قرار می گیرد و پس از اتمام قسمت 1، قبل از قسمت بعدی، اجرا می شود. بازگشت‌های دوره‌ای به حلقه رویداد بین اجرای «شمارش»، هوای کافی را برای موتور جاوا اسکریپت فراهم می‌کند تا بتواند کار دیگری انجام دهد تا به سایر اقدامات کاربر واکنش نشان دهد.

نکته قابل توجه این است که هر دو نوع – با و بدون تقسیم کار با setTimeout – از نظر سرعت قابل مقایسه هستند. تفاوت زیادی در زمان شمارش کلی وجود ندارد.

برای نزدیک‌تر کردن آنها، بیایید بهبودی ایجاد کنیم.

زمان‌بندی را به ابتدای count() منتقل می‌کنیم:

let i = 0;

let start = Date.now();

function count() {

  // move the scheduling to the beginning
  if (i < 1e9 - 1e6) {
    setTimeout(count); // schedule the new call
  }

  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();

حالا وقتی شروع به count() می کنیم و می بینیم که باید