logo
ریکت خودت رو بساز - قسمت دوم
ریکت

ریکت

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 سه اتفاق میوفته:

  1. ابتدا element به DOM اضافه میشه (add the element to the DOM)
  2. برای تمام فرزندان element ها fiber ایجاد میشه (create the fibers for the element’s children)
  3. عملیات بعدی مشخص میشه (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 رو برای قسمت بعدی قرار میدیم.


خوب بچه ها امیدوارم این قسمت براتون مفید بوده باشه. خوشحال میشم اگه انتقادی یا پیشنهادی دارید برام بنویسید تا محتوای بهتری تولید بشه ❤️

منبع: https://pomb.us/build-your-own-react/

این پست برات مفید بود؟

0

heart

0

like

0

happy

0

sad