در قسمت ششم یاد گرفتیم انجام کار را تا زمان نیاز عقب بیندازیم. این بار فقط با یک محاسبه طرف نیستیم. ممکن است چندین تغییر در یک بازه کوتاه اتفاق بیفتند و هر کدام بخواهند کار مشابهی مثل render را اجرا کنند.

در قسمت هفتم می‌بینیم چطور این کارها را در یک batch جمع کنیم و با یک scheduler زمان اجرای آن‌ها را تعیین کنیم. این همان ایده‌ای است که سیستم reactive ما را از اجرای شتاب‌زده و تکراری نجات می‌دهد.

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

Batching چند تغییر نزدیک به هم را در یک بسته جمع می‌کند. Scheduler هم مشخص می‌کند کارهای جمع‌شده چه زمانی و با چه ترتیبی اجرا شوند.

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

یک scheduler کوچک

const jobs = new Set();
let scheduled = false;

function schedule(job) {
  jobs.add(job);

  if (scheduled) return;
  scheduled = true;

  queueMicrotask(() => {
    for (const currentJob of jobs) currentJob();
    jobs.clear();
    scheduled = false;
  });
}

let renders = 0;

function render() {
  renders++;
  console.log(`رندر شماره ${renders}`);
}

schedule(render);
schedule(render);
schedule(render);

// بعد از پایان کد هم‌زمان فقط یک بار چاپ می‌شود: رندر شماره 1

استفاده از Set باعث می‌شود یک job تکراری فقط یک بار در صف بماند. queueMicrotask اجرای صف را تا پایان کد هم‌زمان فعلی عقب می‌اندازد. بنابراین سه فراخوانی پشت سر هم در یک batch قرار می‌گیرند.

متغیر scheduled جلوی ساختن سه microtask جدا را می‌گیرد. بار اول مقدار آن false است، پس callback زمان‌بندی می‌شود. بارهای بعدی فقط job را به Set می‌دهند و برمی‌گردند. بعد از خالی شدن صف، مقدار دوباره false می‌شود تا batch بعدی بتواند زمان‌بندی شود.

چون هر سه بار همان reference تابع render فرستاده شده، Set فقط یک عضو دارد. اگر هر بار یک arrow function تازه بسازیم، Set آن‌ها را سه تابع متفاوت می‌بیند و هر سه اجرا می‌شوند.

کاربرد واقعی

در یک جدول، کاربر چند فیلتر را پشت سر هم تغییر می‌دهد. هر تغییر می‌تواند وضعیت برنامه را همان لحظه اصلاح کند، اما رسم دوباره جدول و محاسبه چیدمان فقط یک بار انجام شود. فریم‌ورک‌های رابط کاربری از شکل‌های پیشرفته‌تر همین ایده استفاده می‌کنند.

نمونه دیگر دریافت چند پیام در یک بازه بسیار کوتاه است. به جای به‌روزرسانی نمودار برای هر پیام، می‌توان پیام‌ها را در state قرار داد و رسم نمودار را یک بار در انتهای batch انجام داد.

Batching و debounce یکی نیستند. Batching کارهای یک بازه کوتاه را با هم اجرا می‌کند. Debounce تا وقتی رویدادها ادامه دارند صبر می‌کند و معمولا فقط آخرین درخواست را نگه می‌دارد. برای ذخیره متن هنگام تایپ، debounce مناسب است. برای یکی کردن چند تغییر state در یک چرخه، batching انتخاب بهتری است.

Scheduler چه چیزهای دیگری را مدیریت می‌کند؟

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

انتخاب صف هم روی رفتار اثر دارد. microtaskها قبل از رفتن مرورگر به task بعدی اجرا می‌شوند. setTimeout کار را به task بعدی می‌برد و requestAnimationFrame آن را به زمان مناسب رسم فریم نزدیک می‌کند. برای تغییرات DOM مرتبط با تصویر، requestAnimationFrame گاهی انتخاب مناسب‌تری است.

یک جزئیات مهم این است که اگر یکی از jobها خطا بدهد، حلقه متوقف می‌شود و پاک‌سازی انتهای callback انجام نمی‌شود. در محصول واقعی بهتر است صف را با try/finally پاک کنیم و برای خطای هر job سیاست مشخصی داشته باشیم.

مراقب گرسنگی صف باشید

ساختن microtask جدید درون microtask می‌تواند فرصت رندر یا رسیدگی به ورودی کاربر را عقب بیندازد. scheduler نباید فقط سریع‌ترین صف را انتخاب کند؛ باید اجازه بدهد مرورگر و کارهای دیگر هم نوبت اجرا بگیرند. در کارهای سنگین، شکستن پردازش به بخش‌های کوچک از یک batch بسیار بزرگ بهتر است.

ادامه دارد

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