ریکت
23 شهریور 1402
ریکت خودت رو بساز - قسمت دوم
توی این قسمت از مجموعه ریکت خودت رو بساز میخوایم این سه قدم برای توسعه ریکت خودمون برداریم:
- قدم سوم: Concurrent Mode
- قدم چهارم: Fibers
- قدم پنجم: Render and Commit Phases
اگه قسمت قبل رو از دست دادی میتونی از این لینک ببینی.
قدم سوم: Concurrent Mode
قبل از اینکه بیشتر جلو بریم بهتره refactor انجام بدیم. ما داخل حلقه بازگشتی که داخل قدم دوم نوشتیم مشکل داریم.
وقتی شروع به رندر کردن میکنیم، تا زمانی که تمام element های درخت را رندر نکنیم، متوقف نمیشویم. اگر این درخت (element tree) بزرگ باشه، عملکرد اصلی برنامه (main thread) رو برای مدت طولانی مسدود یا بلاک میکنه و مرورگر نمیتونه به چیزهایی که اولویت بالاتری دارند مثل handling user input، برسه و باید تا تموم شدن رندر صبر کنه.
element.props.children.forEach(child =>
render(child, dom)
)
ما برای حل این مشکل میایم عملیات ها رو به قسمت های کوچیک تقسیم میکنیم، بعد از تموم شدن هر بخش کوچیک به مروگر اجازه میدیم، تا در صورت نیاز به انجام کاری، رندر کردن رو به وقفه (interrupt) بندازد و یا قطع کند.
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
ما از requestIdleCallback
برای ایجاد لوپ استفاده میکنیم. requestIdleCallback
شبیه به setTimeout
هست، ولی بجای اینکه ما بگیم کی اجرا بشه، مرورگر زمانی که برنامه اصلی (main thread) بیکار (idle) باشه، این callback را صدا میزند. ریکت دیگه از requestIdleCallback
استفاده نمیکنه و بجاش از پکیج scheduler استفاده میکنه، اما از نظر مفهومی یکسان هستند.
همچنین requestIdleCallback
پارامتر deadline رو هم به ما میده که میتونیم از اون برای بررسی اینکه چقدر زمان داریم تا مرورگر نیاز به کنترل مجدد داشته باشه، استفاده کنیم.
برای اینکه لوپ شروع بشه لازم هست که اولین عملیات رو مشخص و تنظیم کنیم، بعد تابع performUnitOfWork
رو بنویسیم که عملیات رو اجرا میکنه و همچنین عملیات بعدی رو به عنوان خروجی مشخص میکنه.
قدم چهارم: Fibers
برای اینکه واحد هایی که کار ما رو انجام میدن رو منظم کنیم، نیاز به data structure داریم - a fiber tree
ما برای هر المنت (element) یک fiber خواهیم داشت و هر fiber یک واحد کار یا عملیات خواهد بود. فرض کنید ما میخوایم همچنین المان هایی رو رندر کنیم:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
داخل render
ما root fiber رو ایجاد میکنیم، و اون رو به عنوان nextUnitOfWork
مشخص میکنیم. بقیه کار داخل تابع performUnitOfWork
اتفاق میوفته، برای هر fiber سه اتفاق میوفته:
- ابتدا element به DOM اضافه میشه (add the element to the DOM)
- برای تمام فرزندان element ها fiber ایجاد میشه (create the fibers for the element’s children)
- عملیات بعدی مشخص میشه (select the next unit of work)
یکی از هدف های این data structure این هست که به راحتی عملیات بعدی (unit of work) مشخص باشه. به همین خاطر هر fiber به اولین فرزند، به والد و المان ایی که والد مشترک با المان دیگه (sibling) داره، لینک هستند.
وقتی کار روی یک fiber را تمام کردیم، اگر فرزند ایی داشته باشه، اون fiber کار بعدی خواهد بود. بطور مثال وقتی که کار ما روی div
fiber تموم شد، عملیات بعدی روی h1
fiber خواهد بود.
اما اگر فرزندی نداشت، از sibling
استفاده میکنیم و اون کار بعدی ما خواهد بود. بطور مثال p
fiber هیچ فرزندی ندارد پس ما بعد از تموم شدن، سراغ a
fiber میریم.
اگر fiber هیچ child
و sibling
نداشت میریم سراغ “uncle”: یعنی sibling
والد (parent). مثل fiber های a
و h2
.
همچنین اگر والد هیچ sibling
نداشت، اینقدر از طریق والد ها بالا میریم تا یکی از اونها sibling
داشته باشه یا به root برسیم. اگر root رسیدیم به معنای این هست که تمام عملیات ها برای رندر انجام شده. حالا بیاید دست به کد بشیم!
ابتدا لازم هست تابع render
که بصورت زیر بود:
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
رو بازنویسی کنیم و قسمتی از اون که برای ایجاد DOM node هستش رو جدا تعریف کنیم:
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
function render(element, container) {
// TODO set next unit of work
}
داخل تابع render
جدید باید root fiber رو به عنوان nextUnitOfWork
مشخص کنیم:
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
هر موقع هم مرورگر آماده بود تابع workLoop
رو صدا میزنه و ما فرایند رو شروع میکنیم. بریم سراغ پیاده سازی تابع performUnitOfWork
.
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
ابتدا لازم هست node جدید ایجاد و به DOM اضافه (append) کنیم. برای اینکه DOM node رو بعدا بهش دسترسی داشته باشیم داخل fiber.dom
قرار میدیم.
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO create new fibers
// TODO return next unit of work
}
بعدش برای هر فرزند یک fiber جدید درست میکنیم:
function performUnitOfWork(fiber) {
...
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
}
// TODO return next unit of work
}
و بسته به اینکه فرزند اول باشه یا نه، اون رو به عنوان child
و یا sibling
به درخت fiber اضافه میکنیم:
...
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// TODO return next unit of work
در نهایت ما کار بعدی رو مشخص میکنیم، ابتدا child
، بعد sibling
، بعد uncle
و...
...
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
و در نهایت این میشه تابع performUnitOfWork
ما:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
قدم پنجم: Render and Commit Phases
اینجا ما یک مشکل دیگه داریم. ما هر بار که داریم تابع performUnitOfWork
رو صدا میزنیم یک node جدید به DOM اضافه میکنیم. چون میدونیم که مروگر میتونه قبل از تموم شدن فرایند رندر، وقفه ایجاد کنه، پس ممکن هست کاربر UI ناقص ببینه و ما این رو نمیخوایم.
ما لازم هست ریشه (root) درخت fiber رو (root of the fiber tree) دنبال کنیم، و اسمش رو wipRoot
یا work in progress root میذاریم:
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
و زمانی که تمام عملیات ها و کار ها تموم شد، میایم درخت fiber رو به DOM اضافه میکنیم:
function commitRoot() {
// TODO add nodes to dom
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// we commit the whole fiber tree to the DOM.
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
پیاده سازی تابع commitRoot
رو برای قسمت بعدی قرار میدیم.
خوب بچه ها امیدوارم این قسمت براتون مفید بوده باشه. خوشحال میشم اگه انتقادی یا پیشنهادی دارید برام بنویسید تا محتوای بهتری تولید بشه ❤️
این پست برات مفید بود؟
0
0
0
0
نظرات