mini-react

300行搭建一个自己的mini-react

  • jsx
  • requestIdlcallback
  • fiber
  • diff
  • useState
  • useEffect

贴上 github-repo https://github.com/lidelong-0201/mini-react

jsx

首先看一下jsx,jsx是React.createElement的语法糖,我们平时写jsx时会被webpack脚手架的@babel/preset-react或vite脚手架的@vitejs/plugin-vue-jsx** **等进行转换,(转换配置可以看一下,之前实现搭建webpack的代码库:repo

<div id='app'>app</div>
//会被处理成
React.createElemnt('div',{id='app'},'app')

例如,我在程序中写了一个打印foo的log,处理之后的打印是这样的 image.png

image.png

来实现React.createElemnt与createTextNode

下面贴上实现代码

function createTextNode(nodeValue) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue,
      children: []
    }
  };
}
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children?.map((child) => {
        const isTextNode = ['string', 'number'].includes(typeof child);
        return isTextNode ? createTextNode(child) : child;
      })
    }
  };
}

fiber

fiber的能力

  • 增量渲染(把渲染任务拆分成块,匀到多帧)
  • 更新时能够暂停,终止渲染任务
  • 并发更新
{
      parent: fiber,
      props: child.props,
      type: child.type,
      sibling: null,
      dom: oldFiberChild.dom,
      child: null,
      altemate: oldFiberChild,
      effectTag: 'UPDATE'|'PLACEMENT', 
      effectHooks:{
   					 callback,
    				 deps, // 依赖项
   					 cleanup: //callback的void,
 									 }[]
 		  stateHooks:{state:any,queue:action}[]
}

image-transformed.png

requestIdleCallback

问题

当有大量dom需要同时渲染的时候,直接去渲染会造成浏览器卡顿被用户所感知

分析问题

页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。 浏览器每一帧都需要完成 image.png

  • 处理用户的交互
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚去等的处理
  • rAF
  • 布局
  • 绘制

解决问题

构建workCallback 函数是一个工作回调函数,它接收一个 deadLine 参数。在函数内部,我们使用一个 while 循环来执行工作单元的任务,直到达到 shouldYield 的条件或者没有下一个工作单元为止。如果 wipRoot 的兄弟节点的类型与 nextWorkOfUnit 的类型相同,我们将 nextWorkOfUnit 设为 null。shouldYield 的值是根据 deadLine.timeRemaining() 是否小于 1 来判断的。如果满足条件,即剩余时间小于 1,shouldYield 将设为 true。 如果 nextWorkOfUnit 为 null 且 wipRoot 不为 null,则执行 commitRoot(),进行渲染操作。 最后,我们使用 window.requestIdleCallback(workCallback) 来请求下一个空闲时间段执行 workCallback。 image.png

具体实现代码

const workCallback = (deadLine) => {
  let shouldYield = false
  while (!shouldYield && nextWorkOfUnit) {
    nextWorkOfUnit = perFormWorkOfUnit(nextWorkOfUnit)
    if (wipRoot?.sibling?.type === nextWorkOfUnit?.type) {
      nextWorkOfUnit = null
    }
    shouldYield = deadLine.timeRemaining() < 1
  }
  // render 时条件满足不会执行多变
  if (!nextWorkOfUnit && wipRoot) {
    commitRoot()
  }
  window.requestIdleCallback(workCallback)

Diff

在 React 进行双树对比时,会根据 alternate 属性来找到当前的 Fiber 节点。 目前实现了组件 diff 和元素 diff。 在对比组件时,判断是否为同一个组件。如果不是同一个组件,会在调和节点上打上 effectTag 标签,以进行删除并重新创建节点。 在对比元素时,判断当前属性 props 是否相同,是否需要更新。如果需要更新,同样在调和节点上打上更新标签。如果判断出不同的 DOM 标签,例如从 <div> 变成了 <p>,则进行删除并重新创建。

image.png

useState

实现useState 需要在fiber上构建stateHooks,通过index来判断是那个state,最后在统一提交节点进行更新

// 每次更新处理function 会重制为【】
let stateHooks = []
let stateIndex = 0
const useState = (initVal) => {
  let currentFiber = wipFunctionFiber
  const oldHook = currentFiber.altemate?.stateHooks[stateIndex]
  const stateHook = {
    state: oldHook ? oldHook.state : initVal,
    // 任务队列 收集统一执行
    queue: oldHook ? oldHook.queue : [],
  }
  stateHooks.push(stateHook)
  stateIndex++
  currentFiber.stateHooks = stateHooks
  // 每次执行React。useState 统一执行action
  stateHook.queue.forEach((item) => {
    stateHook.state = item
  })
  const setState = (val) => {
    // 值相同不更新
    if (val === stateHook.state) return
    stateHook.queue.push(val)
    // stateHook.state = val
    wipRoot = {
      ...currentFiber,
      altemate: currentFiber,
    }
    nextWorkOfUnit = wipRoot
  }
  return [stateHook.state, setState]
}