Skip to content

srclp/mini-react

Repository files navigation

Mini-React 源码实现教程

📚 项目简介

这是一个从零开始实现的 React 迷你版本,通过逐步提交的方式,带你深入理解 React 的核心原理。本项目实现了 React 的核心功能,包括:

  • ✅ Fiber 架构
  • ✅ 可中断的渲染调度器
  • ✅ Virtual DOM 与 Diff 算法
  • ✅ 函数组件
  • ✅ Hooks(useState、useEffect)
  • ✅ 事件处理和属性更新

📖 目录

  1. 第一步:初始化项目
  2. 第二步:实现调度器
  3. 第三步:分离渲染阶段和提交阶段
  4. 第四步:支持函数组件
  5. 第五步:重构函数组件
  6. 第六步:实现属性更新
  7. 第七步:重构属性更新
  8. 第八步:Diff 算法 - 类型不同时的处理
  9. 第九步:Diff 算法 - 删除多余节点
  10. 第十步:Diff 算法 - 边界情况
  11. 第十一步:优化更新流程
  12. 第十二步:实现 useState
  13. 第十三步:useState 支持 Action 队列
  14. 第十四步:优化 useState 避免冗余更新
  15. 第十五步:实现 useEffect
  16. 第十六步:useEffect 支持清理函数

第一步:初始化项目

Commit: a8511e9 - init

📝 功能说明

项目初始化,实现了最基础的 React 功能:

  • createElement:创建虚拟 DOM
  • createTextNode:创建文本节点
  • render:将虚拟 DOM 渲染成真实 DOM

🔑 核心代码

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(e =>
        typeof e === 'string' ? createTextNode(e) : e
      ),
    },
  }
}

function render(el, container) {
  // 创建 DOM 元素
  const dom = el.type === 'TEXT_ELEMENT'
    ? document.createTextNode('')
    : document.createElement(el.type)

  // 设置属性
  Object.keys(el.props).forEach((key) => {
    if (key !== 'children') {
      dom[key] = el.props[key]
    }
  })

  // 递归渲染子元素
  const children = el.props.children || []
  children.forEach(child => render(child, dom))

  // 添加到容器
  container.append(dom)
}

💡 实现原理

  1. JSX 转换:JSX 会被 Babel 转换为 createElement 调用
  2. 虚拟 DOM:createElement 返回一个描述 DOM 结构的 JavaScript 对象
  3. 递归渲染:render 函数递归遍历虚拟 DOM 树,创建真实 DOM

问题:这个版本的 render 是同步的,如果组件树很大,会阻塞浏览器主线程。


第二步:实现调度器

Commit: ae27b14 - feat: scheduler

📝 功能说明

引入 Fiber 架构和调度器,实现可中断的渲染:

  • 使用 requestIdleCallback 在浏览器空闲时工作
  • 将渲染工作分解为小的工作单元(Fiber)
  • 可以在任何时候中断和恢复渲染

🔑 核心代码

let nextUnitOfWork = null
let wipRoot = null // work in progress root

function workLoop(deadline) {
  let shouldYield = false

  while (!shouldYield && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }

  // Fiber 树构建完成,提交到 DOM
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }

  requestIdleCallback(workLoop)
}

function performUnitOfWork(fiber) {
  // 1. 创建 DOM
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber.type)
  }

  // 2. 处理子元素,创建 Fiber
  const children = fiber.props.children
  let prevSibling = null

  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      dom: null,
      parent: fiber,
    }

    if (index === 0) {
      fiber.child = newFiber
    }
    else {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
  })

  // 3. 返回下一个工作单元
  if (fiber.child)
    return fiber.child
  if (fiber.sibling)
    return fiber.sibling

  let nextFiber = fiber.parent
  while (nextFiber) {
    if (nextFiber.sibling)
      return nextFiber.sibling
    nextFiber = nextFiber.parent
  }
}

requestIdleCallback(workLoop)

💡 实现原理

  1. Fiber 数据结构:每个元素对应一个 Fiber 节点,包含:

    • child:第一个子节点
    • sibling:下一个兄弟节点
    • parent:父节点
    • dom:对应的真实 DOM
  2. 工作循环

    • workLoop 在浏览器空闲时执行
    • 每次处理一个 Fiber 节点
    • 如果时间不够,中断并等待下次空闲
  3. 遍历顺序:深度优先遍历

    • 先访问子节点 (child)
    • 再访问兄弟节点 (sibling)
    • 最后回到父节点继续

第三步:分离渲染阶段和提交阶段

Commit: 581bdf0 - refactor: separate render phase from commit phase

📝 功能说明

将 DOM 操作从渲染阶段分离到提交阶段:

  • 渲染阶段(Render Phase):构建 Fiber 树,可中断
  • 提交阶段(Commit Phase):一次性将所有变更应用到 DOM,不可中断

🔑 核心代码

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}

function commitWork(fiber) {
  if (!fiber)
    return

  const parentDOM = fiber.parent.dom
  parentDOM.appendChild(fiber.dom)

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function performUnitOfWork(fiber) {
  // 只创建 DOM,不添加到父节点
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber.type)
    // 移除了 appendChild 操作
  }

  // ... 创建子 Fiber

  return getNextFiber(fiber)
}

💡 实现原理

  1. 为什么要分离?

    • 渲染阶段可能被中断,如果直接操作 DOM,用户会看到不完整的 UI
    • 提交阶段一次性完成,保证 UI 的一致性
  2. 两阶段工作流程

    • Render Phase:构建完整的 Fiber 树
    • Commit Phase:递归遍历 Fiber 树,将所有 DOM 添加到页面

第四步:支持函数组件

Commit: fd6e01b - feat: support function component

📝 功能说明

支持函数组件,区分主机组件(Host Component)和函数组件:

  • 函数组件没有 DOM
  • 函数组件通过调用函数获取子元素

🔑 核心代码

function performUnitOfWork(fiber) {
  const isFunctionComponent = typeof fiber.type === 'function'

  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  }
  else {
    updateHostComponent(fiber)
  }

  return getNextFiber(fiber)
}

function updateFunctionComponent(fiber) {
  // 调用函数组件,获取子元素
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  // 创建 DOM
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber.type)
  }

  const children = fiber.props.children
  reconcileChildren(fiber, children)
}

function commitWork(fiber) {
  if (!fiber)
    return

  // 函数组件没有 DOM,需要向上找到有 DOM 的父节点
  let parentFiber = fiber.parent
  while (!parentFiber.dom) {
    parentFiber = parentFiber.parent
  }

  if (fiber.dom) {
    parentFiber.dom.appendChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

💡 实现原理

  1. 函数组件特点

    • type 是一个函数
    • 没有对应的 DOM 节点
    • 通过调用函数获取子元素
  2. 处理差异

    • 函数组件:调用 fiber.type(fiber.props) 获取子元素
    • 主机组件:直接从 fiber.props.children 获取子元素
  3. 提交阶段注意

    • 函数组件没有 DOM,需要向上查找有 DOM 的祖先节点

第五步:重构函数组件

Commit: c2b6a36 - refactor: function component

📝 功能说明

重构函数组件的处理逻辑,抽取 reconcileChildren 函数,统一子元素的协调过程。

🔑 核心代码

function reconcileChildren(fiber, children) {
  let prevSibling = null

  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      dom: null,
      parent: fiber,
    }

    if (index === 0) {
      fiber.child = newFiber
    }
    else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
  })
}

💡 实现原理

提取公共逻辑,使代码更清晰,为后续实现 Diff 算法做准备。


第六步:实现属性更新

Commit: 19ddc01 - feat: update props

📝 功能说明

支持更新已有的 DOM 元素,而不是每次都重新创建:

  • 记录上一次的 Fiber 树(alternate)
  • 对比新旧 Fiber,决定是更新、新增还是删除
  • 支持事件处理器的绑定

🔑 核心代码

function reconcileChildren(fiber, children) {
  let oldFiber = fiber.alternate?.child
  let prevSibling = null

  children.forEach((child, index) => {
    const isSameType = oldFiber && oldFiber.type === child.type

    let newFiber = null

    if (isSameType) {
      // 更新
      newFiber = {
        type: child.type,
        props: child.props,
        dom: oldFiber.dom, // 复用旧的 DOM
        parent: fiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
      }
    }
    else {
      // 新增
      newFiber = {
        type: child.type,
        props: child.props,
        dom: null,
        parent: fiber,
        alternate: null,
        effectTag: 'PLACEMENT',
      }
    }

    if (index === 0) {
      fiber.child = newFiber
    }
    else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
  })
}

function commitWork(fiber) {
  if (!fiber)
    return

  const parentDOM = getParentDOM(fiber)

  if (fiber.effectTag === 'PLACEMENT' && fiber.dom) {
    parentDOM.appendChild(fiber.dom)
  }
  else if (fiber.effectTag === 'UPDATE' && fiber.dom) {
    updateProps(fiber.dom, fiber.props, fiber.alternate.props)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function updateProps(dom, nextProps, prevProps) {
  // 设置新属性,更新事件处理器
  Object.keys(nextProps).forEach((key) => {
    if (key !== 'children') {
      if (key.startsWith('on')) {
        const eventType = key.slice(2).toLowerCase()
        dom.addEventListener(eventType, nextProps[key])
      }
      else {
        dom[key] = nextProps[key]
      }
    }
  })
}

💡 实现原理

  1. effectTag:标记 Fiber 的操作类型

    • PLACEMENT:新增
    • UPDATE:更新
  2. alternate:指向上一次渲染的 Fiber

    • 通过对比 alternate 和当前 Fiber 实现 Diff
  3. DOM 复用

    • 类型相同:复用旧 DOM,只更新属性
    • 类型不同:创建新 DOM

第七步:重构属性更新

Commit: 57e04a6 - refactor: update props

📝 功能说明

完善属性更新逻辑,支持属性删除和事件处理器的正确更新:

  • 删除旧属性
  • 移除旧的事件监听器
  • 添加新的事件监听器

🔑 核心代码

function updateProps(dom, nextProps, prevProps = {}) {
  // 1. 删除旧属性
  Object.keys(prevProps).forEach((key) => {
    if (key !== 'children') {
      if (!(key in nextProps)) {
        dom.removeAttribute(key)
      }
    }
  })

  // 2. 设置新属性
  Object.keys(nextProps).forEach((key) => {
    if (key !== 'children') {
      if (nextProps[key] !== prevProps[key]) {
        if (key.startsWith('on')) {
          const eventType = key.slice(2).toLowerCase()
          // 移除旧的事件监听器
          dom.removeEventListener(eventType, prevProps[key])
          // 添加新的事件监听器
          dom.addEventListener(eventType, nextProps[key])
        }
        else {
          dom[key] = nextProps[key]
        }
      }
    }
  })
}

💡 实现原理

三种属性变更情况:

  1. 旧有新无:删除属性
  2. 新有旧无:添加属性
  3. 新旧都有但值不同:更新属性

对于事件处理器,需要先移除旧的再添加新的,避免内存泄漏。


第八步:Diff 算法 - 类型不同时的处理

Commit: 07521ac - feat: diff, update children if type is different

📝 功能说明

完善 Diff 算法,支持元素类型变化时的删除和新增操作。

🔑 核心代码

function reconcileChildren(fiber, children) {
  let oldFiber = fiber.alternate?.child
  let prevSibling = null

  children.forEach((child, index) => {
    const isSameType = oldFiber && oldFiber.type === child.type

    let newFiber = null

    if (isSameType) {
      // 更新
      newFiber = { /* ... */ }
    }
    else {
      // 类型不同,需要删除旧的,创建新的
      if (child) {
        newFiber = { /* 新增 */ }
      }

      if (oldFiber) {
        deletions.push(oldFiber) // 标记为删除
      }
    }

    // ...
  })
}

function commitRoot() {
  deletions.forEach(commitDeletion) // 先删除
  commitWork(wipRoot.child) // 再新增和更新
  wipRoot = null
  deletions = []
}

function commitDeletion(fiber) {
  if (fiber.dom) {
    const parentDOM = getParentDOM(fiber)
    parentDOM.removeChild(fiber.dom)
  }
  else {
    // 函数组件没有 DOM,递归删除子节点
    commitDeletion(fiber.child)
  }
}

💡 实现原理

  1. 删除操作

    • 将要删除的 Fiber 收集到 deletions 数组
    • 在 Commit 阶段统一处理删除
  2. 类型变化

    • 旧 Fiber 标记删除
    • 创建新 Fiber 并标记为 PLACEMENT

第九步:Diff 算法 - 删除多余节点

Commit: 9582039 - feat: diff, delete unused dom

📝 功能说明

处理新子元素比旧子元素少的情况,删除多余的旧子元素。

🔑 核心代码

function reconcileChildren(fiber, children) {
  let oldFiber = fiber.alternate?.child
  let prevSibling = null

  children.forEach((child, index) => {
    // ... 对比逻辑

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
  })

  // 删除剩余的旧子元素
  while (oldFiber) {
    deletions.push(oldFiber)
    oldFiber = oldFiber.sibling
  }
}

💡 实现原理

遍历完新的子元素后,如果还有剩余的旧子元素,说明它们需要被删除。


第十步:Diff 算法 - 边界情况

Commit: e0e8cfa - feat: diff, edge case

📝 功能说明

处理特殊情况:

  • 子元素为 nullundefined
  • 子元素是数字类型

🔑 核心代码

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((e) => {
        const isTextNode = typeof e === 'string' || typeof e === 'number'
        return isTextNode ? createTextNode(e) : e
      }),
    },
  }
}

function reconcileChildren(fiber, children) {
  // ...
  children.forEach((child, index) => {
    // ...
    } else {
      if (child) { // 检查 child 是否存在
        newFiber = { /* 新增 */ }
      }

      if (oldFiber) {
        deletions.push(oldFiber)
      }
    }
    // ...
  })
}

💡 实现原理

添加空值检查,确保不会为 nullundefined 创建 Fiber 节点。


第十一步:优化更新流程

Commit: 52d0460 - feat: optimize update

📝 功能说明

优化组件更新流程,实现精准更新:

  • 只更新变化的组件及其子树
  • 避免重新渲染整个应用

🔑 核心代码

function workLoop(deadline) {
  let shouldYield = false

  while (!shouldYield && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)

    // 优化:如果下一个 work 和当前 root 的 sibling 相同
    // 说明已经遍历完当前这棵子树了,可以停止
    if (nextUnitOfWork?.type === wipRoot?.sibling?.type) {
      nextUnitOfWork = null
    }

    shouldYield = deadline.timeRemaining() < 1
  }

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

  requestIdleCallback(workLoop)
}

💡 实现原理

通过检查是否遍历完当前子树,避免不必要的更新,提升性能。


第十二步:实现 useState

Commit: dc78e3d - feat: useState

📝 功能说明

实现 useState Hook,支持函数组件的状态管理。

🔑 核心代码

let wipFiber = null
let stateHookIndex = 0
let stateHooks = []

function updateFunctionComponent(fiber) {
  stateHooks = []
  stateHookIndex = 0
  wipFiber = fiber

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function useState(initialValue) {
  const currentFiber = wipFiber

  const oldStateHook = currentFiber.alternate?.stateHooks[stateHookIndex]
  const stateHook = {
    state: oldStateHook?.state || initialValue,
  }

  stateHookIndex++
  stateHooks.push(stateHook)
  currentFiber.stateHooks = stateHooks

  function setState(value) {
    stateHook.state = value

    // 触发重新渲染
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }
    nextUnitOfWork = wipRoot
  }

  return [stateHook.state, setState]
}

💡 实现原理

  1. Hook 存储

    • 每个函数组件有自己的 Hook 数组(stateHooks
    • 使用索引(stateHookIndex)区分多个 useState
  2. 状态保持

    • alternate.stateHooks 获取上一次的状态
    • 初次渲染时使用 initialValue
  3. 触发更新

    • setState 修改状态后,创建新的 wipRoot
    • 触发新一轮渲染

第十三步:useState 支持 Action 队列

Commit: 2f0f23c - feat: useState action queue

📝 功能说明

支持在同一次事件中多次调用 setState,使用 Action 队列批量处理更新。

🔑 核心代码

function useState(initialValue) {
  const currentFiber = wipFiber

  const oldStateHook = currentFiber.alternate?.stateHooks[stateHookIndex]
  const stateHook = {
    state: oldStateHook?.state || initialValue,
    queue: oldStateHook?.queue || [], // Action 队列
  }

  // 执行队列中的所有 action
  stateHook.queue.forEach((action) => {
    stateHook.state = action(stateHook.state)
  })
  stateHook.queue = []

  stateHookIndex++
  stateHooks.push(stateHook)
  currentFiber.stateHooks = stateHooks

  function setState(action) {
    const actionFunc = typeof action === 'function' ? action : () => action

    stateHook.queue.push(actionFunc) // 加入队列

    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }
    nextUnitOfWork = wipRoot
  }

  return [stateHook.state, setState]
}

💡 实现原理

  1. Action 队列

    • 将每次 setState 转换为 action 函数存入队列
    • 下次渲染时统一执行队列中的所有 action
  2. 支持函数式更新

    • setState(value):直接设置值
    • setState(prev => prev + 1):基于前一个状态计算
  3. 批量更新

    • 多次 setState 只触发一次渲染
    • 确保状态更新的正确性

第十四步:优化 useState 避免冗余更新

Commit: 8d14dcd - feat: useState, prevent redundant state updates

📝 功能说明

优化 useState,如果新值和旧值相同,则不触发重新渲染。

🔑 核心代码

function setState(action) {
  const actionFunc = typeof action === 'function' ? action : () => action

  // 提前计算新值
  const eagerState = actionFunc(stateHook.state)

  // 如果值没有变化,不触发更新
  if (eagerState === stateHook.state) {
    return
  }

  stateHook.queue.push(actionFunc)

  wipRoot = {
    ...currentFiber,
    alternate: currentFiber,
  }
  nextUnitOfWork = wipRoot
}

💡 实现原理

优化策略

  • 在触发更新前,先计算新值
  • 如果新值和当前值相同,直接返回,不触发渲染
  • 避免不必要的组件更新,提升性能

第十五步:实现 useEffect

Commit: a9abaf9 - feat: useEffect

📝 功能说明

实现 useEffect Hook,支持副作用处理。

🔑 核心代码

let effectHooks = []

function updateFunctionComponent(fiber) {
  effectHooks = []
  stateHooks = []
  stateHookIndex = 0
  wipFiber = fiber

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function useEffect(callback, deps) {
  const effectHook = {
    callback,
    deps,
  }

  effectHooks.push(effectHook)
  wipFiber.effectHooks = effectHooks
}

function commitRoot() {
  deletions.forEach(commitDeletion)
  commitWork(wipRoot.child)
  commitEffectHooks() // 在 DOM 更新后执行
  wipRoot = null
  deletions = []
}

function commitEffectHooks() {
  function run(fiber) {
    if (!fiber)
      return

    // 初次渲染,直接执行
    if (!fiber.alternate) {
      fiber.effectHooks?.forEach((hook) => {
        hook.callback()
      })
    }
    else {
      // 检查依赖是否变化
      fiber.effectHooks?.forEach((newHook, index) => {
        const oldHook = fiber.alternate.effectHooks[index]

        const needUpdate = oldHook?.deps.some((oldDep, i) => {
          return oldDep !== newHook.deps[i]
        })

        if (needUpdate) {
          newHook.callback()
        }
      })
    }

    run(fiber.child)
    run(fiber.sibling)
  }

  run(wipRoot)
}

💡 实现原理

  1. 执行时机

    • 在 Commit 阶段完成后执行
    • 确保 DOM 已经更新
  2. 依赖检查

    • 初次渲染:直接执行
    • 更新时:对比依赖数组,有变化才执行
  3. 依赖对比

    • 使用浅比较(!==)检查每个依赖项
    • 任意一个依赖变化都会触发回调

第十六步:useEffect 支持清理函数

Commit: ab036ff - feat: useEffect, support cleanup

📝 功能说明

支持 useEffect 的清理函数,用于清理副作用(如取消订阅、清除定时器等)。

🔑 核心代码

function useEffect(callback, deps) {
  const effectHook = {
    callback,
    deps,
    cleanup: undefined, // 存储清理函数
  }

  effectHooks.push(effectHook)
  wipFiber.effectHooks = effectHooks
}

function commitEffectHooks() {
  function run(fiber) {
    if (!fiber)
      return

    if (!fiber.alternate) {
      fiber.effectHooks?.forEach((hook) => {
        hook.cleanup = hook.callback() // 保存清理函数
      })
    }
    else {
      fiber.effectHooks?.forEach((newHook, index) => {
        if (newHook.deps?.length === 0)
          return // 空数组,只执行一次

        const oldHook = fiber.alternate.effectHooks[index]

        const needUpdate = oldHook?.deps.some((oldDep, i) => {
          return oldDep !== newHook.deps[i]
        })

        if (needUpdate) {
          newHook.cleanup = newHook.callback()
        }
      })
    }

    run(fiber.child)
    run(fiber.sibling)
  }

  function runCleanup(fiber) {
    if (!fiber)
      return

    // 执行上一次的清理函数
    fiber.alternate?.effectHooks?.forEach((hook) => {
      if (hook.deps?.length !== 0) { // deps 为空数组时,卸载时才清理
        hook.cleanup?.()
      }
    })

    runCleanup(fiber.child)
    runCleanup(fiber.sibling)
  }

  runCleanup(wipRoot) // 先清理
  run(wipRoot) // 再执行
}

💡 实现原理

  1. 清理函数

    • useEffect 的回调可以返回一个清理函数
    • 清理函数在下次 effect 执行前调用
  2. 执行顺序

    1. 执行上一次的清理函数
    2. 执行新的 effect 回调
    3. 保存新的清理函数
    
  3. 特殊情况

    • deps 为空数组 []:只在首次渲染执行,清理函数在组件卸载时执行
    • deps:每次渲染都执行

🎯 总结

通过这 16 个提交,我们从零实现了一个 mini-react,涵盖了 React 的核心概念:

核心特性

  1. Fiber 架构:可中断的渲染,提升用户体验
  2. 调度器:利用 requestIdleCallback 在浏览器空闲时工作
  3. Diff 算法:高效对比虚拟 DOM,最小化 DOM 操作
  4. HooksuseStateuseEffect 的完整实现
  5. 两阶段提交:分离渲染和提交,保证 UI 一致性

关键概念

  • Virtual DOM:用 JavaScript 对象描述 UI
  • Reconciliation:对比新旧虚拟 DOM,找出差异
  • Fiber:React 的工作单元,支持增量渲染
  • Effect:副作用管理,支持清理函数

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published