Last updated on

mini-react 实现

从 0 开始实现一个最简单版本的 React

  • 支持 Function Component
  • 支持 useState useReducer

从 入口 开始

import App from "./App.tsx"

ReactDOM.createRoot(document.getElementById("root")!).render(<App />)
function App() {
  return <>This is App</>;
}

export default App;

先来实现一个 react-dom.js

function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot
}

ReactDOMRoot.prototype.render = function (children) {
  const root = this._internalRoot
  console.log("sedationh render", root, children)
}

function createRoot(container) {
  const root = { containerInfo: container }

  return new ReactDOMRoot(root)
}

export default { createRoot }

render 函数可以拿到

image

<App /> 会 形成一个对象「Virtual DOM」,大概是下面的样子,更详细的内容 可看 或者 这里

这一层是由编译来做的,被转为一个函数调用,返回对象

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

支持 HostComponent、仅仅创建流程,最简 React

大白话就是只渲染 原生 DOM

ReactDOM.createRoot(document.getElementById("root")!).render(
  <div>
    <h1>App</h1>
    <a href="https://baidu.com">baidu</a>
    This is App
  </div>
)

在 render 后 去 updateContainer

function updateContainer(element, container) {
  const { containerInfo } = container
  const fiber = createFiber(element, {
    type: containerInfo.nodeName.toLocaleLowerCase(),
    stateNode: containerInfo,
  })
  // 组件初次渲染
  scheduleUpdateOnFiber(fiber)
}

ReactDOMRoot.prototype.render = function (children) {
  const root = this._internalRoot
  console.log("sedationh render", root, children)
  + updateContainer(children, root)
}

containerInfo 就是 document.getElementById("root")

  const fiber = createFiber(element, {
    type: containerInfo.nodeName.toLocaleLowerCase(),
    stateNode: containerInfo,
  })

这里用 containerInfo 直接创建了一个 root node 的 fiber

export function createFiber(vnode, returnFiber) {

createFiber 会利用 Virtual DOM 和 returnFiber 构建 fiber 结构

returnFiber 会给 return 构建 fiber 关系

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    // 父亲节点
    return: returnFiber,
export function createFiber(vnode, returnFiber) {
  console.log("sedationh createFiber", vnode, returnFiber)
  const fiber = {
    // 类型
    type: vnode.type,
    key: vnode.key,
    // 属性
    props: vnode.props,
    // 不同类型的组件, stateNode也不同
    // 原生标签 dom节点
    // class 实例
    stateNode: null,

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    // 父亲节点
    return: returnFiber,

    flags: Placement,

    // 记录节点在当前层级下的位置
    index: null,
  }

  if (isString(vnode.type)) {
    fiber.tag = HostComponent
  }

  return fiber
}

形成的 fiber 会喂给 scheduleUpdateOnFiber

let wip = null // work in progress 当前正在工作中的
let wipRoot = null

export function scheduleUpdateOnFiber(fiber) {
  wip = fiber
  wipRoot = fiber
}

到这里,render 的调用栈就结束了,requestIdleCallback 登场

/**
 - @param {IdleDeadline} idleDeadline
 */
function workLoop(idleDeadline) {
  while (wip && idleDeadline.timeRemaining() > 0) {
    performUnitOfWork()
  }

  if (!wip && wipRoot) {
    commitRoot()
  }
}

requestIdleCallback(workLoop)

这个可以理解为死循环一直执行 workLoop 函数,但只会在浏览器的渲染进程空闲的时候进行

RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。 目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。 故 RequestIdleCallback 定位处理的是: 不重要且不紧急的任务

workLoop 干两件事情

  • performUnitOfWork
    • 找不同,看看要干哪些活,准备活「工作单元(fiber)」
  • commitRoot
    • 干活啦,进行 DOM 更改

以下为 GPT 的说法 在 React Fiber 架构中,workLoop  函数是一个循环,用于驱动 React 应用程序的工作进程。它负责执行两个主要任务:performUnitOfWork  和  commitRoot

  1. performUnitOfWork

    • performUnitOfWork  是  workLoop  的第一个任务。
    • 它的作用是执行当前工作单元(fiber)的工作并返回下一个工作单元。
    • 一个工作单元代表了 React 中的一个组件或元素,需要进行处理、更新或渲染。
    • 在执行工作单元期间,会根据不同的工作类型和组件类型执行相应的操作,如调用函数组件、类组件的生命周期方法,处理更新队列,创建子工作单元等。
    • 当一个工作单元的工作完成后,performUnitOfWork  会返回下一个要执行的工作单元,以便继续进行下一轮的工作。
  2. commitRoot

    • commitRoot  是  workLoop  的第二个任务。
    • 它在所有工作单元都被处理完毕后被调用,用于将更新结果提交到实际的 DOM 中。
    • 在  commitRoot  中,React 会遍历整个 Fiber 树,将需要更新的 DOM 节点进行插入、更新或删除操作,以反映应用程序的最新状态。
    • 这个过程通常涉及到底层的 DOM 操作,如创建新的 DOM 节点、更新属性、添加事件监听器等。
    • 一旦  commitRoot  执行完成,React 应用程序的界面就会得到更新,并呈现给用户。

通过  performUnitOfWork  和  commitRoot  的交替执行,React 能够以递增的方式处理组件的更新,同时保持对用户界面的响应和流畅度。这种增量更新的方式也是 React Fiber 架构的核心思想之一。

需要注意的是,workLoop  函数还可能执行其他任务,如处理错误、调度优先级等,但  performUnitOfWork  和  commitRoot  是其最重要的两个任务,负责驱动 React 应用程序的工作。 GPT 说法结束

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break

    default:
      break
  }

  // dfs
  if (wip.child) {
    wip = wip.child
    return
  }

  let next = wip
  while (next) {
    if (next.sibling) {
      wip = next.sibling
      return
    }

    next = next.return
  }

  wip = null
}

performUnitOfWork 以 dfs 的方式走 fiber 链

image

遍历过程中,根据 fiber 结构中的 tag 进行区分

ReactWorkTags.js

export const FunctionComponent = 0
export const ClassComponent = 1
export const IndeterminateComponent = 2 // Before we know whether it is function or class
export const HostRoot = 3 // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4 // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5
export const HostText = 6
export const Fragment = 7
export const Mode = 8
export const ContextConsumer = 9
export const ContextProvider = 10
export const ForwardRef = 11
export const Profiler = 12
export const SuspenseComponent = 13
export const MemoComponent = 14
export const SimpleMemoComponent = 15
export const LazyComponent = 16
export const IncompleteClassComponent = 17
export const DehydratedFragment = 18
export const SuspenseListComponent = 19
export const ScopeComponent = 21
export const OffscreenComponent = 22
export const LegacyHiddenComponent = 23
export const CacheComponent = 24
export const TracingMarkerComponent = 25

updateHostComponent 的工作是两块

  • updateNode -> 创建 DOM ,存在 stateNode 里,更新 DOM 属性、处理文本内容
  • reconcileChildren 根据 Arrau<Vritual DOM> children 接着完善 fiber 链
export function updateHostComponent(wip) {
  if (!wip.stateNode) {
    wip.stateNode = document.createElement(wip.type)
    updateNode(wip.stateNode, wip.props)
  }

  reconcileChildren(wip, wip.props.children)
}

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = nextVal[key]
      }
    } else {
      node[key] = nextVal[key]
    }
  })
}

function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let previousNewFiber = null
  for (let i = 0; i < newChildren.length; i++) {
    const newChild = newChildren[i]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, wip)

    if (previousNewFiber === null) {
      // head node
      wip.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }
}!

接下来看 commitRoot 的工作。以 dfs 的方式进行 DOM 改动,改动的流程就是找父亲节点,然后吧在 performUnitOfWork 中准备的 DOM 节点塞进去

function commitRoot() {
  commitWorker(wipRoot)
  wipRoot = null
}

function commitWorker(wip) {
  if (!wip) {
    return
  }

  // 1. 提交自己
  const parentNode = getParentNode(wip.return)
  const { flags, stateNode } = wip
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode)
  }
  // 2. 提交子节点
  commitWorker(wip.child)
  // 3. 提交兄弟
  commitWorker(wip.sibling)
}

function getParentNode(wip) {
  let p = wip
  while (p) {
    if (p.stateNode) {
      return p.stateNode
    }

    p = p.return
  }
}

至此,完成 「支持 HostComponent、仅仅创建流程,最简 React」

看效果 👇

ReactDOM.createRoot(document.getElementById("root")!).render(
  <div>
    <h1>App</h1>
    <a href="https://baidu.com">baidu</a>
    This is App
  </div>
)

image

支持 FunctionComponent && ClassComponent && HostText

createFiber 的时候,增加 fiber.tag = FunctionComponent

export function createFiber(vnode, returnFiber) {
  console.log("sedationh createFiber", vnode, returnFiber)
  const fiber = {
    // 类型
    type: vnode.type,
    key: vnode.key,
    // 属性
    props: vnode.props,
    // 不同类型的组件, stateNode也不同
    // 原生标签 dom节点
    // class 实例
    stateNode: null,

    // 第一个子fiber
    child: null,
    // 下一个兄弟节点
    sibling: null,
    return: returnFiber,

    flags: Placement,

    // 记录节点在当前层级下的位置
    index: null,
  }

  if (isString(vnode.type)) {
    fiber.tag = HostComponent
  }
  if (isFunction(vnode.type)) {
    fiber.tag = FunctionComponent
  }

  return fiber
}

然后在 performUnitOfWork 处理 FunctionComponent,调用函数,返回值作为 children 接着构建 fiber 链

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break
    case FunctionComponent:
      updateFunctionComponent(wip)
      break

export function updateFunctionComponent(wip) {
  const { type, props } = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

接下来 ClassComponent 和 HostText

function performUnitOfWork() {
  const { tag } = wip

  switch (tag) {
    case HostComponent:
      updateHostComponent(wip)
      break
    case FunctionComponent:
      updateFunctionComponent(wip)
      break
    case ClassComponent:
      updateClassComponent(wip)
      break
    case HostText:
      updateHostComponent(wip)
      break

export function updateClassComponent(wip) {
  const { type, props } = wip
  const instance = new type(props)
  const children = instance.render()
  reconcileChildren(wip, children)
}

export function updateHostTextComponent(wip) {
  wip.stateNode = document.createTextNode(wip.props.children)
}

解释下 HostText 「就是 同级 下有至少两个的节点、且有文本节点的情况」,如下面的 「有其他同级元素的文本」,「App」 不算

    <div>
      <h1>App</h1>
      <a href="https://baidu.com">baidu</a>
      有其他同级元素的文本
      {/* @ts-ignore */}
      <ClassComp />
    </div>

「App」 的这种情况在 updateNode 的时候进行了处理

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        // STUDY: seda 文本节点处理
        node.textContent = nextVal[key]
      }
    } else {
      node[key] = nextVal[key]
    }
  })
}

并且会在 reconcileChildren 的时候进行返回

function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

Fragment 的处理比较简单,略

引入最小堆和更新队列

前面提到requestIdleCallback工作只有 20FPS,一般对用户来感觉来说,需要到 60FPS 才是流畅的, 即一帧时间为 16.7 ms,所以这也是react团队自己实现requestIdleCallback的原因。实现大致思路是在requestAnimationFrame获取一桢的开始时间,触发一个postMessage,在空闲的时候调用idleTick来完成异步任务。 — https://juejin.cn/post/6844904196345430023#heading-11

React Scheduler 为什么使用 MessageChannel 实现

先来实现一个最小堆吧 https://github.com/sedationh/code-playground/blob/871f11bdda338ecdbe8921d6a086d5086a3d33d9/ts/src/Heap/index.ts

image

基本思路可参考 https://github.com/kodecocodes/swift-algorithm-club/blob/master/Heap/README.markdown

先去除之前用 requestIdleCallback 的调用方式

function workLoop() {
  while (wip) {
    performUnitOfWork()
  }

  if (!wip && wipRoot) {
    commitRoot()
  }
}

// requestIdleCallback(workLoop)

workLoop 的触发时机是在

export function scheduleUpdateOnFiber(fiber) {
  wip = fiber
  wipRoot = fiber

  scheduleCallback(workLoop)
}

scheduleCallback 负责调度我们的 workLoop

实现如下

import { Heap } from "./heap"

let taskIdCounter = 0

const taskHeap = new Heap((parent, child) => {
  if (parent.expirationTime === child.expirationTime) {
    return parent.id < child.id
  }
  return parent.expirationTime < child.expirationTime
})

export function scheduleCallback(callback) {
  const currentTime = getCurrentTime()
  const timeout = -1

  const expirationTime = currentTime - timeout

  const newTask = {
    id: taskIdCounter,
    callback,
    expirationTime,
  }
  taskIdCounter += 1

  taskHeap.add(newTask)

  requestHostCallback()
}

const channel = new MessageChannel()
function requestHostCallback() {
  channel.port1.postMessage(null)
}
channel.port2.onmessage = function () {
  workLoop()
}

function workLoop() {
  let currentTask = taskHeap.pop()

  while (currentTask) {
    const callback = currentTask.callback
    callback()
    currentTask = taskHeap.pop()
  }
}

export function getCurrentTime() {
  return performance.now()
}

我们传入的 workLoop 作为 scheduleCallbackcallback 进入,被组织为 task 加入 taskHeap

添加 hooks 能力

useReducer

下面是我们测试自己写的 useReducer 的例子

function FunctionComp() {
  const [cnt, dispatchCnt] = useReducer((state, action) => {
    console.log("sedationh action", action, n++)
    return state + 1
  }, 0)

  const [cnt2, dispatchCnt2] = useReducer((state, action) => {
    console.log("sedationh action", action, n++)
    return state + 2
  }, 0)

  return (
    <div>
      <button
        onClick={() => {
          dispatchCnt("action")
        }}
      >
        dispatchCnt cnt1
      </button>
      {cnt}
      <hr />
      <button
        onClick={() => {
          dispatchCnt2("action")
        }}
      >
        dispatchCnt cnt2
      </button>
      {cnt2}
      <h1>FunctionComp</h1>
    </div>
  )
}

先简单处理下事件行为 「onClick」

export function updateNode(node, nextVal) {
  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = nextVal[key]
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.addEventListener(eventName, nextVal[key])
    } else {
      node[key] = nextVal[key]
    }
  })
}

hook 的结构如下

    hook = {
      memoriedState: null, // 状态
      next: null, // 下一个 hook
    }

关键逻辑

  • 找到当前 hook
  • 找到当前 hook 所在的 fiber
  • 在 hook 所在的 fiber 上标记 老 fiber「alternate」
  • 利用 reducer 去更新新的 hook.memoriedState
  • 利用 scheduleUpdateOnFiber 以当前更新 fiber 节点为 rootFiber 进行更新处理
export const useReducer = (reducer, initialState) => {
  const hook = updateWorkInProgressHook()

  if (!currentlyRenderingFiber.alternate) {
    // 初次渲染
    hook.memoriedState = initialState
  }

  const dispatch = (action) => {
    hook.memoriedState = reducer(hook.memoriedState, action)
    currentlyRenderingFiber.alternate = { ...currentlyRenderingFiber }
    scheduleUpdateOnFiber(currentlyRenderingFiber)
  }

  return [hook.memoriedState, dispatch]
}

为了能满足上面的代码,需要在 fiber 上增加一些信息

export function createFiber(vnode, returnFiber) {
  const fiber = {

...

    // old fiber
    alternate: null,

    // function component, hook0
    memoriedState: null,
  }

...
let currentlyRenderingFiber = null
let workInProgressHook = null

function updateWorkInProgressHook() {
  let hook

  const current = currentlyRenderingFiber.alternate
  if (!current) {
    // 初次渲染
    hook = {
      memoriedState: null,
      next: null,
    }

    if (!workInProgressHook) {
      // hook0
      workInProgressHook = currentlyRenderingFiber.memoriedState = hook
    } else {
      // hook1, hook2, hook3 ...
      workInProgressHook = workInProgressHook.next = hook
    }
  } else {
    // 更新
    currentlyRenderingFiber.memoriedState = current.memoriedState
    if (!workInProgressHook) {
      // hook0
      hook = workInProgressHook = currentlyRenderingFiber.memoriedState
    } else {
      // hook1, hook2, hook3 ...
      hook = workInProgressHook = workInProgressHook.next
    }
  }

  return hook
}

currentlyRenderingFiber 怎么拿到呢?

export const renderWithHooks = (wip) => {
  currentlyRenderingFiber = wip
  currentlyRenderingFiber.memoriedState = null
  workInProgressHook = null
}
export function updateFunctionComponent(wip) {
  renderWithHooks(wip)

  const { type, props } = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

接下来在 reconcileChildren 中进行 diff,进行更新标记

// 协调(diff)
// 创建新 fiber
function reconcileChildren(wip, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let oldFiber = wip.alternate?.child
  let previousNewFiber = null
  for (let i = 0; i < newChildren.length; i++) {
    const newChild = newChildren[i]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, wip)
    const same = isSame(oldFiber, newFiber)
    if (same) {
      Object.assign(newFiber, {
        alternate: oldFiber,
        stateNode: oldFiber.stateNode,
        flags: Update,
      })
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (previousNewFiber === null) {
      // head node
      wip.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }
}

接下来完善 commit 环节

function commitWorker(wip) {
  if (!wip) {
    return
  }

  // 1. 提交自己
  const parentNode = getParentNode(wip.return)
  const { flags, stateNode } = wip
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode)
  }
  if (flags & Update && stateNode) {
    updateNode(stateNode, wip.alternate.props, wip.props)
  }
  // 2. 提交子节点
  commitWorker(wip.child)
  // 3. 提交兄弟
  commitWorker(wip.sibling)
}

因为需要比较,所有原来的 updateNode 也要改下

export function updateNode(node, prev, nextVal) {
  Object.keys(prev).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        node.textContent = ""
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.removeEventListener(eventName, prev[key])
    } else {
      node[key] = ""
    }
  })

  Object.keys(nextVal).forEach((key) => {
    if (key === "children") {
      if (isStringOrNumber(nextVal[key])) {
        // STUDY: seda 文本节点处理
        node.textContent = nextVal[key]
      }
    } else if (key.slice(0, 2) === "on") {
      const eventName = key.slice(2).toLowerCase()
      node.addEventListener(eventName, nextVal[key])
    } else {
      node[key] = nextVal[key]
    }
  })
}

现在来整体看下流程

  • 初次渲染 Function 组件
    • render
    • updateContainer
    • scheduleUpdateOnFiber
    • workLoop
    • performUnitOfWork
    • updateFunctionComponent
    • renderWithHooks
    • 调用 Function 组件
    • useReducer
    • reconcileChildren
    • commitWorker
  • 更新
    • dispatch
    • scheduleUpdateOnFiber

useState

思路是差不多的

export const useState = (initialState) => {
  const hook = updateWorkInProgressHook()

  if (!currentlyRenderingFiber.alternate) {
    // 初次渲染
    hook.memoriedState = initialState
  }

  const fiber = currentlyRenderingFiber

  const dispatch = (newState) => {
    hook.memoriedState = typeof newState === "function" ? newState(hook.memoriedState) : newState
    fiber.alternate = { ...fiber }
    fiber.sibling = null
    scheduleUpdateOnFiber(fiber)
  }

  return [hook.memoriedState, dispatch]
}

完善更新能力

目前的更新能力都是通过位置来比较的,具体来说如

new abcd
old cd

a !== c b !== d

这俩都会被删除

export function createFiber(vnode, returnFiber) {
  const fiber = {

...

	// 处理删除
    deletoins: null,
  }
function reconcileChildren(returnFiber, children) {
  if (isStringOrNumber(children)) {
    return
  }

  const newChildren = isArray(children) ? children : [children]
  let oldFiber = returnFiber.alternate?.child
  let previousNewFiber = null
  let newIndex
  for (newIndex = 0; newIndex < newChildren.length; newIndex++) {
    const newChild = newChildren[newIndex]
    if (newChild == null) {
      continue
    }
    const newFiber = createFiber(newChild, returnFiber)
    const same = isSame(oldFiber, newFiber)
    if (same) {
      Object.assign(newFiber, {
        alternate: oldFiber,
        stateNode: oldFiber.stateNode,
        flags: Update,
      })
    }
    if (!same && oldFiber) {
      // 删除节点
      deleteChild(returnFiber, oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (previousNewFiber === null) {
      // head node
      returnFiber.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }

    previousNewFiber = newFiber
  }

  /**
   - new ab
   - old abcdef
   *
   - 通过遍历 ab 是走不到 cdef 的
   */
  if (newIndex === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber)
    return
  }
}

function deleteRemainingChildren(returnFiber, currentFirstChild) {
  let childToDelete = currentFirstChild
  while (childToDelete) {
    deleteChild(returnFiber, childToDelete)
    childToDelete = childToDelete.sibling
  }
}