اگر React و Chrome بچه داشتند. (بخش اول، ژن React)

خیلی سریع میرم سر اصل مطلب، تو این مقاله میخوام راجب API ای صحبت کنم که حاصل همکاری ری‌اکت و کرومه، web scheduling API.
اما قبلش یکم پیش زمینه نیازه، همه چیز از یه پکیج شروع شد:

پکیج Scheduler

پکیج Scheduler توسط تیم فیسبوک ساخته شده، داخل ریپازیتوری خود‌ ری‌اکت maintain میشه و قراره تو زمینه Cooperative Scheduling بهمون کمک کنه. این پکیج بشدت داخل سورس React استفاده میشه و همین الان هم می‌تونیم اون رو با دستور npm i scheduler نصب کنیم.

چی؟ Cooperative Scheduling چیه؟

هممون میدونیم که جاوااسکریپت یدونه Thread بیشتر نداره، یعنی در لحظه فقط ۱ کار میتونه انجام بده. و داخل مرورگر علاوه بر اجرای جاوااسکریپت، تقریبا همه چیزهای دیگه‌ی وبسایت از validate کردن فرم‌ها گرفته تا ریکوئست زدن به سرور و به حرکت در‌آوردن انیمیشن ها و پخش گیف و ویدیو و اسکرول(در بعضی موارد) و حتی سلکت کردن متن بر عهده همین یک Threadعه. Cooperative Scheduling بهمون کمک میکنه از Threadمون نهایت استفاده رو ببریم.
درواقع کانسپت Cooperative Scheduling (یا cooperative multitasking) به معنی "برنامه ریزی مشارکتی" ? از علوم کامپیوتر قرض گرفته شده و تو حوزه سیستم‌های عامل‌ از مدت ها پیش حضور داشته (قبل از اختراع ویندوز یا حتی لینوکس) و خیلی شناخته شده‌اس. قضیه اینه که میخوایم همین یدونه ترد رو طوری "برنامه ریزی" کنیم که همه بطور "مشارکتی" ازش استفاده کنن. نقل قول از ویکیپدیا:

Cooperative multitasking, is a style of computer multitasking in which processes voluntarily yield control periodically or when idle or logically blocked in order to enable multiple applications to be run concurrently.

یعنی چی؟ فرض کنید cpu لپتاپ من فقط یک core داشته باشه. اما من بعد از بالا اومدن سیستم عامل،‌ میخوام همزمان هم تلگرامم رو باز داشته باشم، هم مرورگرم رو. اگه به لطف تکنیک هایی مثل همین cooperative scheduling نبود (تو سیستم عامل های متداول از تکنیک دیگه ای به اسم preemptive multitasking استفاده میشه)، همچین چیزی امکان نداشت. چون process تلگرام به محض اینکه باز میشد، cpu رو مشغول میکرد و تا زمانی که بسته نشده به هیچ برنامه دیگه‌ای اجازه نمیداد از منابع سیستم استفاده کنه.
اما این تکنیک ها چجوری مشکل رو حل می‌کنن؟ با ایجاد کردن "توهم" همزمانی. به هرکدوم از برنامه‌های ما یه زمانی برای استفاده از cpu اختصاص میدن، و بعد کنترل رو به برنامه بعدی پاس میدن. اینجوری در هر ثانیه چندبار کنترل cpu بین اپ های مختلف جابجا میشه و اینطور بنظر میرسه که بطور همزمان دارن کار میکنن، ولی درواقعیت اینطور نیست. همون یک ترد موجود بین اپ های مختلف تقسیم شده، و سیستم عامل میتونه ضمن تقسیم کردن منابع، تصمیم بگیره به اپی که تو حالت focus عه اولویت بیشتری بده، یا اولویت اپی که minimize شده رو کم کنه، بدون اینکه یوزر یا دولوپر اون اپ ها متوجه چیزی بشن (ری‌اکت قراره در آینده با concurrent mode اینکار رو برامون انجام بده)

باشه. ولی مگه مرورگر سیستم‌عامله؟

بله.
مرورگر سیستم عاملیه بر روی سیستم‌عامل ها (شاید در‌آینده مستقل‌تر ببینیمش?). درواقع مرورگر همون چیزیه که یه زمان James Gosling با ساختن JVM آرزوش رو داشت. یه runtime جهانی،‌ فارغ از سخت‌افزار و نرم‌افزار موجود.

و درنتیجه برای سریع‌تر کردن برناممون، به تکنیک‌های مشابه نیاز داریم. و اینجاست که نقش scheduler مشخص میشه.

نقل قول از داکیومنت scheduler:

This is a package for cooperative scheduling in a browser environment.

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

The public API for this package is not yet finalized.

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

برای دستیابی به یه CPU آزاد که در اون هر تسکی فارغ از نژاد و ملیتش بتونه به‌ راحتی کارهاش رو انجام بده (?) نیازمند انجام ۲ تا کاره:

۱- همه تسک‌هامون رو تا حد امکان کوچیک کنیم، و اگه کاری هست که انجام شدنش زمان زیادی می‌بره، یا از تکنیک هایی مثل WebWorker استفاده کنیم و یا اون رو به کارهای کوچیکتر تقسیم کنیم.
۲- بتونیم به تسک‌های مختلف اولویت‌های مختلف بدیم (اونقدرام فارغ از نژاد نیست گویا ?)، بعنوان مثال کاربر بعد از کلیک کردن روی یه دکمه، برای دیدن نتیجه تعاملش مجبور نباشه منتظر بمونه تا ما رندر کردن یه بخش دیگه از صفحه رو کامل تموم کنیم، یا عدد دو میلیاردم فیبوناچی رو حساب کنیم.

پکیج Scheduler چجوری کمکمون میکنه؟

پکیج Scheduler فووووووووووووق العاده ساده‌اس(در عین نبوغ) و از سه تا فایل اصلی تشکیل میشه:

1- /packages/scheduler/src/Scheduler.js

این فایل API پکیج scheduler رو شامل میشه و بخش اعظم لاجیک اولویت بندی‌ها تو این فایل پیاده شده.

2- /packages/scheduler/src/forks/SchedulerHostConfig.default.js

کدهای لازم برای ارتباط Scheduler با پلتفرمی که روش اجرا میشه، (مرورگر یا نود) و زمان‌بندی فریم‌های برنامه داخل این فایل قرار دارن.

3- /packages/react-reconciler/src/SchedulerWithReactIntegration.js

یه لایه ارتباطی خیلی نازک بین پکیج scheduler و react عه، برای اینکه کدها decoupled بمونن و افراد خارج از react هم بتونن از scheduler استفاده کنن.

بریم ببینیم این فایل‌ها دقیقا چیکار میکنن:

Scheduler.js

مهم‌ترین تابعی که از این فایل اکسپورت میشه(که مهم‌ترین تابع کل پکیج هم هست) فانکشن scheduleCallback عه. این تابع ۳ تا ورودی میگیره: priorityLevel که اولویت کار یا همون تابعی که داریم schedule میکنیم رو مشخص میکنه، callback که تابعیه که قراره کارمون رو انجام بده، و options که یه آبجکته با دوتا فیلد delay و timeout که هردو آپشنالن و برای ما خیلی مهم نیستن.
priorityLevel می‌تونه یکی از مقادیر زیر رو داشته باشه:

ImmediatePriority, UserBlockingPriority, MediumPriority, LowPriority و IdlePriority
(به ترتیب از اولویت بالا به پایین)

بعد از گرفتن ورودی‌ها، تابع scheduleCallback بلافاصله برای priorityLevel ای که بهش دادیم، با استفاده از تابع timeoutForPriorityLevel مقدار timeout رو حساب میکنه. کد این تابع تقریبا شبیه قطعه کد زیره:

سورس کد تابع timeoutForPriorityLevel
سورس کد تابع timeoutForPriorityLevel

و بعد با استفاده از فرمول زیر، expirationTime رو حساب میکنه:

const expirationTime = getCurrentTime() + timeout;

اما این ینی چی؟ آیا معنیش اینه که اگه تسکی با اولویت Normal داشته باشیم، بعد از 5 ثانیه انجام میشه؟
به هیچ وجه.
همه تسک‌ها، با توجه به اولویتشون، در اولین فرصتی که پیدا کنن انجام میشن; اما timeout برای جلوگیری از اتفاقی به اسم starvation ساخته شده.
برای اینکه متوجه بشیم starvation چیه باید جلوتر بریم تا ببینیم دقیقا چجوری از timeout استفاده شده.

تابع scheduleCallback بعد از محاسبه timeout،‌ برای callback ما یه آبجکت به نام task میسازه. مهم‌ترین فیلد های این آبجکت چهارتا فیلد callback, priorityLevel, startTime و expirationTime ان. و بعد از ساخته شدن هر تسک، اون رو داخل یه minHeap به اسم timerQueue قرار میده و expirationTime رو بعنوان sortIndex ست میکنه.(اگه نمیدونید داده‌ساختار minHeap چیه این منبع میتونه براتون مفید باشه. ولی می‌تونید اینجوری تصور کنید که minHeap یه آرایه‌اس، که همیشه از بزرگ به کوچیک مرتب شده، درنتیجه تو مثال ما که sortIndex برابر expirationTimeعه، همیشه اولین عضو آرایه عضویه که کوچیکترین expirationTime رو داره، ینی از همه تسک‌ها زودتر expire میشه)

بعد از اضافه کردن task به heap،‌ تابع scheduleCallback یکی از تابع های فایل SchedulerHostConfig به اسم requestHostCallback رو صدا میزنه و تابعی به اسم flushWork رو بهش ورودی میده.

فعلا بیاید اینطور "فرض" کنیم که این تابع صرفا همچین کاری می‌کنه:

function requestHostCallback(fn) {
    setTimeout(fn, 0);
}

پس اگه ما بهش flushWork رو ورودی بدیم، بلافاصله توی فاز بعدی timer ها تابع مارو صدا میزنه.

خوشبختانه کد واقعی requestHostCallback به این سادگی نیست (پایینتر دقیق بررسیش می‌کنیم) و دوتا آرگومان به flushWork ما ورودی میده: اولی hasTimeRemaining که مشخص می‌کنه آیا هنوز توی این فریم زمان داریم یا نه، و دومی initialTime که timestamp (یا به عبارت دقیق‌تر DOMHighResTimestamp) زمان فعلی برنامه، در لحظه اجرا شدن تابع رو مشخص می‌کنه.

کاری که تابع flushWork می‌کنه اینه که تابع workLoop رو صدا می‌زنه و اونجا عضو ریشه minHeap (عضو با کمترین expirationTime) رو برمیداره، توی متغیر task میریزدش و چک می‌کنه ببینه آیا

task.expirationTime <= initialTime

هست یا خیر. (دقت کنید کدهای واقعی کمی تفاوت دارن، ولی نتیجه یکسانه و من سعی می‌کنم ساده ترین حالت رو توضیح بدم)

اگر شرط بالا برقرار باشه، callback تسک صرف‌نظر از اینکه آیا hasTimeRemaining = true هست یا نه صدا زده میشه. و چک کردن ریشه minHeap انقدر تکرار میشه تا دیگه تسکی که از expirationTime اش گذشته باشه داخل minHeap وجود نداشته باشه یا بعبارتی همه تسک های expire شده انجام شده باشن.

بعد workLoop یکی دیگه از توابع SchedulerHostConfig به اسم shouldYieldToHost رو صدا میزنه تا چک کنه آیا هنوز هم وقت اضافه‌ای برای انجام کارهامون داریم؟ اگر جواب مثبت بود دوباره سراغ minHeap میره و عضو ریشه‌اش رو برمیداره و callback متناظرش رو صدا می‌زنه. این عملیات هم تا اونجایی ادامه پیدا می‌کنه که یا دیگه تسکی داخل minHeap وجود نداشته باشه، و یا تابع shouldYieldToHost مقدار false برگردونه.
در این حالت بسته به اینکه آیا هنوز تسکی داخل minHeap داریم یا نه،‌ یکی از دو مقدار true یا false رو ریترن می‌کنیم و درواقع به SchedulerHostConfig میگیم که آیا درآینده بازهم کاری برای انجام دادن داریم یا نه.

چیزی که تااینجا از رفتار تابع workLoop متوجه شدیم اینه که اگه timeout یه تسک Normal برابر ۵ ثانیه‌اس، معنیش این نیست که تابع ما بعد از ۵ ثانیه اجرا میشه،‌ بلکه معنیش اینه که اگر بیشتر از ۵ ثانیه گذشته باشه و تابع ما هنوز فرصت اجرا شدن پیدا نکرده باشه، scheduler در اولین فرصت تابع ما (و همه توابع expire شده) رو صرف‌نظر از اینکه آیا اجراشون باعث دیرتر جواب دادن به interaction های کاربر، یا کمی لگ زدن انیمیشن‌ها میشه یا نه، اجرا می‌کنه.

بعنوان مثال اگه تابعی با اولویت NormalPriority داشته باشیم و scheduler کار دیگه‌ای نداشته باشه، تابع ما بلافاصله (درواقع بعد از اجرا شدن setTimeout فرضیمون)‌ اجرا میشه. اما اگه سر scheduler شلوغ باشه و ۱۰ تا تابع با اولویت UserBlockingPriority داشته باشه، NormalPriority باید صبر کنه تا یا 1) همه اون‌ها انجام شن یا 2) انقضاش بگذره، تا همشون پشت سرهم داخل فریم فعلی شروع به اجرا شدن کنن. که در‌اینصورت معنیش اینه که همه توابع UserBlocking ما هم از انقضاشون گذشته (چون توی minHeap بالاتر از Normal ما قرار داشتن و درنتیجه expirationTime کوچکتری دارن)
تو مثال بالا اگه حین پردازش تسک های UserBlocking، یه تسک Immediate به scheduler مون اضافه بشه، چون timeout اش برابر منفی یکه، تو minHeap بالاتر از بقیه تسک های UserBlocking قرار می‌گیره و قبل از اون‌ها انجام میشه. یا اگه بعد از پردازش UserBlocking ها و قبل از پردازش تسک Normal مون،‌ یه تسک UserBlocking جدید به minHeap اضافه بشه، به احتمال خیلی زیاد جلوتر از تسک Normal ای که درحال حاضر توی minHeap هست قرار میگیره (مگر اینکه تا expirationTime تسک Normal ما کم‌تر از ۲۵۰ میلی ثانیه باقیمونده باشه)

خب این ساختاری که توضیح دادم، هیچوقت مشکل Starving براش پیش نمیاد. یعنی چی؟ سیستمی رو فرض کنید که تسک هارو فقط به ترتیب اولویت انجام بده. مثلا فقط و فقط به شرطی تسک Normal رو انجام بده که هیچ تسک UserBlocking ای وجود نداشته باشه. توی این سیستم به مشکل starving میخوریم،‌ ینی ممکنه حالتی پیش بیاد که مثلا هر تسک UserBlocking،‌ یه تسک UserBlocking دیگه رو schedule کنه. پس تا ابد تسک های UserBlocking خودشون رو میسازن و هیچوقت نوبت به تسک Normal ما نمیرسه. اما تو سیستم فعلی scheduler، حتی اگه همچین اتفاقی بیافته، تسک Normal مابالافاصله بعد از اینکه به فاصله کم‌تر از ۲۵۰ میلی ثانیه تا expiration اش برسه، از همه تسک های UserBlocking ای که بعد از این schedule میشن اولویت بالاتری پیدا می‌کنه و قطعا انجام میشه. یا بعبارتی، هیچوقت مشکل starving پیش نمیاد.

امیدوارم خوب مفهوم رو منتقل کرده باشم.

کوچیک کردن تسک ها

تا الان چندبار ذکر کردیم، یکی از بخش‌های طراحی cooperative scheduling،‌ اولویت بندی کردن task هاست. اما پکیج scheduler تو این موضوع چجوری بهمون کمک می‌کنه؟

مثال زیر رو ببینید: (کدسندباکس)

https://codesandbox.io/s/react-scheduler-fibo-animation-hfu8x

تو این مثال یه فانکشن داریم که سعی می‌کنه ۲۰۰ امین عدد فیبوناچی رو با یه الگوریتم عادی O n حساب کنه، اما از عمد داخل هر بار اجرای forای که فیبوناچی رو حساب می‌کنه، فانکشن delay رو صدا زدم که یه while خالی رو ۵۰ میلیون بار میچرخه و درنتیجه الگوریتم رو بشدت کند می‌کنه.

همزمان با این فانکشن، تابع animate رو صدا زدیم که با استفاده از javascript مربع قرمز رنگی که داخل صفحه داریم رو animate می‌کنه، روش کار اینجوریه که lastTime رو ذخیره می‌کنیم، بعد توی هر تیک requestAnimationFrame مقدار زمانی که از lastTime گذشته رو پیدا می‌کنیم و حساب می‌کنیم که مربعمون چند پیکسل باید جابجا بشه و درآخر box.style.left رو با مقدار بدست اومده ست می‌کنیم.

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

حالا اجرای فانکشن blockingFibo رو با fibo جایگزین کنید.(خط ۷۲) بعد از ریلود متوجه میشید که حتی درحین محاسبه عدد فیبوناچی هم، انیمیشن تقریبا smooth پخش میشه (اگر هنوز لگ دارید مشکل از بیش از حد طولانی بودن while پنج میلیون تاییه، که احتمالا روی دیوایستون نمیشه طی مدت زمان کم‌تر از ۱۰۰ میلی ثانیه اجراش کرد)

چجوری موفق شدیم اینکار رو بکنیم؟ برای دستیابی به این نتیجه از ۳ تا API مختلف scheduler استفاده کردیم.

1- unstable_scheduleCallback

همونجوری که می‌بینید داخل فانکشن fibo یه تابع داریم به اسم work، که این تابع رو خودمون صدا نکردیم،‌در عوض رفرنس تابع رو برای scheduleCallback فرستادیم و اجازه دادیم scheduler با استفاده از minHeap اش بهش رسیدگی کنه. ضمنا priorityLevel رو هم روی Idle ست کردیم،‌ چون تسک مهمی نیست و میخوایم هروقت مرورگر کار دیگه‌ای برای انجام دادن نداشت، یکم وقت هم روی حساب کردن فیبوناچی بذاره پس این لول براش مناسبه.

2- unstable_shouldYield

این تابع هم یکی از تابع هاییه که از خود Scheduler.js اکسپورت شده و توی کدش اول چک می‌کنه ببینه آیا تسکی با اولویت بالاتر از تسکی که الان درحال انجام دادنش هستیم داریم،‌ یا نه. درصورتی که تسکی با اولویت بالاتر داشتیم true ریترن می‌کنه و درغیر اینصورت از تابع shouldYieldToHost فایل SchedulerHostConfig.js استفاده می‌کنه و نتیجه اون رو برمیگردونه.
پس با استفاده از این تابع می‌تونیم بعد از هر حلقه for چک کنیم که آیا میتونیم بریم سراغ حلقه بعدی، یا باید کنترل رو به scheduler برگردونیم و بقیه کار رو بعدا انجام بدیم.

3- continuation

این کانسپت رو تا الان ندیده بودیم. continuationCallback یه تابعیه که قراره ادامه تسکی که درحال حاضر یه بخشیش انجام شده رو انجام بده. نحوه ست کردنش به این شکله که ما می‌تونیم از تابعی که به scheduleCallback دادیم یه تابع ریترن کنیم، و این تابع بعنوان continuationCallback تسک اولیه set میشه. درواقع continuation هم مثل بقیه تسک ها داخل minHeap قرار می‌گیره، ولی expirationTime و همه ویژگی‌هاش، دقیقا برابر همون تسکیه که منجر به ایجاد این continuation شده.

توی مثال فیبوناچیمون هم داخل for چک کردیم تا اگه جواب shouldYield برابر true بود، خود تابع work رو ریترن کنیم که ادامه کار تو فرصت دیگه‌ای دنبال بشه.

SchedulerHostConfig.js

بالاتر گفتیم که این فایل وظیفه هماهنگ کردن scheduler با پلتفرمی که روش اجرا میشه رو برعهده داره، و یسری فانکشن export می‌کنه مثل requestHostCallback, shouldYieldToHost و getCurrentTime.

اما پشت این فانکشن‌ها، کار نسبتا پیچیده‌ای انجام میگیره: تخمین مدت‌زمان هر فریم و مدیریتش.
درواقع بخاطر این فایله که توی مثال قبلی انیمیشن ما بدون لگ اجرا میشد، چون SchedulerHostConfig داره تلاش می‌کنه framerate رو روی 60Hz نگه داره. و این‌کار رو از طریق یه حقه خیلی جذاب انجام میده.

اگه یادتون باشه بالاتر، تابع scheduleCallback اومد و تابع flushWork خودش رو به یکی از توابع SchedulerHostConfig به اسم requestHostCallback پاس داد. اونموقع فرض کردیم که این تابع صرفا setTimeout می‌کنه با مقدار صفر. حالا، این کد واقعی(ساده‌شده)ی تابع requestHostCallback عه:

سورس کد requestHostCallback
سورس کد requestHostCallback

اول از همه callback ورودیش(flushWork) رو داخل یه متغیر گلوبال ذخیره می‌کنه، و بعد اگه RAFLoopRunning نباشه، run اش می‌کنه و تابعی رو ست می‌کنه که تنها پارامتر کالبک‌های requestAnimationFrame ینی rAFTime رو به تابع onAnimationFrame میده.

میدونیم که تابع requestAnimationFrame یه کالبک ورودی می‌گیره، و اون رو برای هر فریمی که رندر می‌کنه صدا میزنه، شاید حدود ۶۰ بار درثانیه، شاید کم‌تر و شاید بیشتر. درواقع طبق step 11.8 این specification:

https://html.spec.whatwg.org/multipage/webappapis.html#step1

توابع rAF، آخر هر فریم اجرا میشن و یکی از آخرین api هایین که صدا زده میشن. و تنها آرگومان ورودی تابعی که به rAF پاس داده شده، rAFTime عه، یه DOMHighResTimestamp که تایم‌استمپِ زمانی که مرورگر شروع به صدا زدن animation frame ها کرده رو نمایش میده. اما چرا‌ scheduler اینکارو می‌کنه و چرا نیاز داره آخر هر فریم صدا زده بشه؟

برای اینکه خودش رو تطبیق بده. با چی؟ با framerate مرورگر و دستگاه. درواقع تابع onAnimationFrame توی این خط ها، داره با توجه به زمان بین دو rAF متوالی، تخمین میزنه که توی فریم بعدی چقدر زمان برای انجام دادن کار خواهم داشت. اول کارشو با 30fps شروع می‌کنه و بعد با توجه به framerate مرورگر و با استفاده از همین rAF ها، خودش رو با framerate موجود تطبیق میده. همچنین زمان شروع هر frame + طول تخمینی frame رو به اسم frameDeadline ذخیره می‌کنه و بعدا، داخل تابع shouldYieldToHost، چک می‌کنه ببینه آیا از frameDeadline گذشتیم یا نه (در نتیجه انیمیشن اون جعبه قرمز تو مثالمون، خیلی روون اجرا میشد)

آخرین کاری که می‌کنه هم استفاده از port.postMessage عه. این چند خط رو ببینید:

استفاده از MessageChannel برای زمانبندی تسک ها
استفاده از MessageChannel برای زمانبندی تسک ها

کلاس MessageChannel یکی از API های مرورگره(داکیومنت) که اجازه میده از نقطه A برای نقطه B پیام بفرستیم (یجورایی شاید بشه گفت Message broker)، اما اینجا این نکته اصلا مهم نیست، چون هردوی نقطه A و B توی همین فایل قرار دارن. چیزی که برای ما مهمه، زمان ارسال پیامه.
طبق این specification:

https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports

هر port یه port message queue داره که یه صف از پیام هاییه که برای این port ارسال شده، و همونطور که داخل specification ذکر کرده، ماهیت واقعی این صف، task source عه. task source ها درواقع متناظرن با task queue ها و دسته‌بندی‌ هایی هستن بر روی task هایی که داخل event loop مون باید انجام شه.
اما نکته مهم، یکی از side effect های ارسال پیام از طریق MessageChannelعه. با اینکار، تابع ای که باری port1 تنظیم شده،‌ اوایل فریم بعدی صدا زده میشه، پس ما میتونیم اوایل فریم، کنترل رو دست بگیریم (راه حل جایگزین استفاده از requestIdleCallback بود، ولی مرورگر ها تو زمینه صدا زدن rIC خیلی تنبلن و خیلی کم‌ صداش می‌زنن)

حالا اگه نگاهی به تابع performWorkUntilDeadline بندازیم، (هرچند از اسمش هم معلومه چیکار می‌کنه) میبینیم که scheduledHostCallback ای که داخل requestHostCallback به خاطر سپرده بودیم رو صدا میزنه (درواقع همون flushWork داخل Scheduler.js) و دوتا پارامتر hasTimeRemaining و currentTime رو براش میفرسته. که از اینجا به بعد ماجرا رو هم یاد گرفتیم که چه اتفاقاتی میافته، Scheduler.js شروع می‌کنه به خالی کردن minHeap، و همچنان یه گوشه چشمی هم به تابع shouldYieldToHost داره که ببینه آیا باید کنترل رو برگردونه یا نه. (درواقع آیا frameDeadline <= currentTime هست یا نه)

امیدوارم مقاله مفیدی بوده باشه، اگه تا اینجا دنبال کردید نوشته رو بهتون تبریک میگم، احتمالا شما جزو ۴ درصدی ها هستید :)

هر سوال، انتقاد، پیشنهاد، ابهامی بود، خوشحال میشم باهم درارتباط باشیم:

@Eddie_CooRo