🦄 用 MiniReact 手撸 Fiber 架构:从零到一的高并发渲染之旅

103 阅读10分钟

"如果你觉得 React 很神秘,那是因为你还没亲手撸过 MiniReact!"

前言

本文将带大家深度拆解 MiniReact 的源码实现,从零开始理解 React Fiber 架构的核心原理。无论你是刚入门的前端开发者,还是想深入理解 React 内部机制的工程师,都能从这份精简而完整的实现中获得启发。

一、项目结构一览 👀

MiniReact 项目结构清晰明了:

mini-react/
├── src/
│   ├── index.jsx         // 入口 JSX 文件
│   └── mini-react.js     // MiniReact 核心实现
├── dist/
│   └── src/
│       ├── index.js      // 编译后的入口 JS
│       ├── mini-react.js // 编译后的 MiniReact
│       └── index.html    // Demo 页面
├── tsconfig.json         // TypeScript 配置
├── readme.md             // 项目说明

二、JSX 到 Fiber:一条龙变身记 🐲

1. JSX 编译原理

在 tsconfig.json 里,我们指定了:

"jsxFactory": "MiniReact.createElement"

这意味着所有 JSX 都会被编译成 MiniReact.createElement(...) 的形式,这正是我们接下来要实现的核心函数。

2. 虚拟 DOM 的创建(createElement & createTextNode

/**
 * 创建 React 元素(虚拟 DOM 节点)
 * 这是 JSX 转换的核心函数,类似于 React.createElement
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // 处理子元素:字符串和数字转换为文本节点,其他保持原样
      children: children.map(child => {
        const isTextNode = typeof child === 'string' || typeof child === "number"
        return isTextNode ? createTextNode(child) : child
      })
    }
  }
}

/**
 * 创建文本节点的虚拟 DOM 表示
 */
function createTextNode(text) {
  return {
    type: 'TEXT_ELEMENT', // 特殊类型标识文本节点
    props: {
      nodeValue: text, // 文本内容
      children: [] // 文本节点没有子元素
    }
  }
}

讲解

  • createElement 是 JSX 转换的核心,负责把标签和属性转成虚拟 DOM 对象
  • 子元素如果是字符串或数字,会被包装成 TEXT_ELEMENT,方便统一处理
  • createTextNode 专门处理文本节点,保证虚拟 DOM 结构的一致性

三、Fiber 架构核心解析

"链表结构让高并发成为可能,递归渲染已成过去式!"

Fiber 本质上是把树结构转换为链表结构,每个节点都记录 parent、child、sibling 信息,这样就能实现渲染过程的中断与恢复,为时间分片奠定基础。

1. Fiber 架构核心变量

// ========== Fiber 架构的核心全局变量 ==========
let nextUnitOfWork = null // 下一个要处理的工作单元(Fiber节点),实现时间分片的关键
let wipRoot = null // work in progress root,正在构建的新 Fiber 树根节点
let currentRoot = null  // 当前已渲染到页面的 Fiber 树根节点,用于 diff 比较
let deletions = null // 存储需要删除的 Fiber 节点数组

讲解

  • nextUnitOfWork:下一个要处理的 Fiber 节点,是实现时间分片的关键
  • wipRoot:正在构建的新 Fiber 树根节点
  • currentRoot:当前已渲染到页面的 Fiber 树根节点,用于 diff 算法比较
  • deletions:存储需要删除的 Fiber 节点列表

2. 渲染入口 render

/**
 * 渲染函数 - React 应用的入口点
 * 初始化 Fiber 树的构建过程
 */
function render(element, container) {
  // 创建新的 Fiber 树根节点
  wipRoot = {
    dom: container, // 对应的真实 DOM 节点
    props: {
      children: [element] // 要渲染的元素作为子节点
    },
    alternate: currentRoot // 指向旧的 Fiber 树,用于 diff 比较
  }
  deletions = [] // 重置删除列表
  nextUnitOfWork = wipRoot // 开始工作循环
}

讲解

  • 初始化 Fiber 树,准备开始构建过程
  • alternate 属性指向旧的 Fiber 树,为后续的 diff 算法做准备
  • 设置 nextUnitOfWork 启动工作循环

四、时间分片与工作循环

⏳ 利用浏览器空闲时间处理任务,避免长时间阻塞主线程

1. 时间分片调度 workLoop

/**js
 * 工作循环 - React Fiber 架构的核心调度器
 * 实现时间分片,避免长时间阻塞主线程
 */
function workLoop(deadline) {
  let shouldYield = false
  // 在有工作单元且时间充足时持续工作
  while (nextUnitOfWork && !shouldYield) {
    // 处理当前工作单元,返回下一个工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    // 检查剩余时间,少于1ms时让出控制权
    shouldYield = deadline.timeRemaining() < 1
  }
  // 所有工作单元处理完毕,开始提交阶段
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()  // 将 Fiber 树的变更应用到真实 DOM
  }
  // 递归调度下一次工作循环
  requestIdleCallback(workLoop)
}
// 启动时间分片调度
requestIdleCallback(workLoop)

讲解

  • 利用 requestIdleCallback 在浏览器空闲时处理任务
  • 每次只处理一个工作单元(Fiber 节点)
  • 当剩余时间不足 1ms 时,主动让出主线程,避免页面卡顿
  • 所有工作单元处理完毕后,进入提交阶段(commit)

2. 单元处理 performUnitOfWork

/**
 * 处理单个工作单元(Fiber 节点)
 * 实现深度优先遍历的 Fiber 树构建
 */
function performUnitOfWork(fiber) {
  // 判断是否为函数组件
  const isFunctionComponent = fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber) // 处理函数组件
  } else {
    updateHostComponent(fiber) // 处理原生 DOM 元素
  }
  // 深度优先遍历:优先处理子节点
  if (fiber.child) {
    return fiber.child
  }
  // 没有子节点时,寻找兄弟节点或回到父节点
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling // 处理兄弟节点
    }
    nextFiber = nextFiber.return // 回到父节点
  }
}

讲解

  • 根据 Fiber 类型区分处理函数组件和原生 DOM 元素
  • 采用深度优先遍历策略:先处理子节点,再处理兄弟节点,最后回到父节点
  • 返回下一个工作单元,实现工作循环的持续进行

五、提交阶段:应用 DOM 变更

提交阶段是将 Fiber 树中的变更应用到真实 DOM 的过程,这一阶段不能被中断。

1. 提交阶段核心函数

/**
 * 提交阶段 - 将 Fiber 树的变更应用到真实 DOM
 * 这是 React 渲染的第二个阶段(第一阶段是 render/reconciliation)
 */
function commitRoot() {
  // 先处理所有需要删除的节点
  deletions.forEach(commitWork)
  // 递归提交所有变更到 DOM
  commitWork(wipRoot.child)
  // 更新当前树的引用
  currentRoot = wipRoot
  wipRoot = null
  deletions = []
}

/**
 * 递归提交单个 Fiber 节点的变更
 * 根据 effectTag 执行相应的 DOM 操作
 */
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  // 向上查找有 DOM 节点的父 Fiber(函数组件没有对应的 DOM)
  let domParentFiber = fiber.return
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.return
  }
  const domParent = domParentFiber.dom
  // 根据副作用标签执行相应操作
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // 新增节点:添加到父节点
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 更新节点:更新属性和事件
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  } else if (fiber.effectTag === "DELETION") {
    // 删除节点
    commitDeletion(fiber, domParent)
  }
  // 递归处理子节点和兄弟节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

/**
 * 处理节点删除操作
 * 需要特殊处理函数组件(没有对应 DOM 节点)
 */
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    // 直接删除有 DOM 节点的 Fiber
    domParent.removeChild(fiber.dom)
  } else {
    // 函数组件没有 DOM,递归删除其子节点
    commitDeletion(fiber.child, domParent)
  }
}

讲解

  • commitRoot 是提交阶段的入口,负责协调所有变更的应用
  • commitWork 根据 Fiber 节点的 effectTag 执行相应的 DOM 操作(新增、更新、删除)
  • commitDeletion 专门处理节点删除,对没有 DOM 的函数组件进行特殊处理
  • 提交阶段采用递归方式处理整个 Fiber 树

六、组件处理与 DOM 操作

1. 函数组件与原生组件更新

// ========== 函数组件处理相关 ==========
let wipFiber = null // 当前正在工作的 fiber 节点(用于 Hooks)
let stateHookIndex = null // 状态钩子的索引(用于 useState 等 Hooks)

/**
 * 更新函数组件
 * 执行函数组件并处理其返回的 JSX
 */
function updateFunctionComponent(fiber) {
  wipFiber = fiber // 设置当前工作的 Fiber(Hooks 需要)
  stateHookIndex = 0 // 重置 Hook 索引
  wipFiber.stateHooks = [] // 初始化状态 Hooks 数组
  wipFiber.effectHooks = [] // 初始化副作用 Hooks 数组
  // 执行函数组件,获取其返回的 JSX
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)  // 将函数组件的子元素集成到 Fiber 树中
}

/**
 * 更新原生 DOM 元素组件
 * 为 Fiber 节点创建对应的 DOM 节点
 */
function updateHostComponent(fiber) {
  // 如果还没有创建 DOM 节点,则创建
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  // 处理子元素的协调
  reconcileChildren(fiber, fiber.props.children)
}

讲解

  • 函数组件通过执行函数获取返回的 JSX,原生组件直接创建对应的 DOM 节点
  • 两种组件最终都会调用 reconcileChildren 进行子元素的协调
  • 为 Hooks 系统预留了状态存储(stateHooks 和 effectHooks

2. DOM 节点创建与属性更新

/**
 * 根据 Fiber 节点创建对应的 DOM 节点
 */
function createDom(fiber) {
  // 根据类型创建相应的 DOM 节点
  const dom = fiber.type === 'TEXT_ELEMENT' ?
    document.createTextNode('') : // 文本节点
    document.createElement(fiber.type) // 普通元素节点
  // 更新 DOM 节点的属性和事件
  updateDom(dom, {}, fiber.props)
  return dom
}

// ========== DOM 属性处理的辅助函数 ==========
const isEvent = key => key.startsWith('on') // 判断是否为事件属性
const isProperty = key => key !== 'children' && !isEvent(key) // 判断是否为普通属性
const isNew = (prevProps, newProps) => (key) => prevProps[key] !== newProps[key] // 判断属性是否有变化
const isGone = (prevProps, newProps) => (key) => !(key in newProps) // 判断属性是否被删除

/**
 * 更新 DOM 节点的属性和事件监听器
 * 比较新旧 props,只更新发生变化的部分
 */
function updateDom(dom, prevProps, newProps) {
  // 防止 props 为 null 或 undefined
  prevProps = prevProps || {}
  newProps = newProps || {}
  
  // 移除旧的事件监听器
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key => !(key in newProps) || isNew(prevProps, newProps)(key)
    )
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2) // 去掉 'on' 前缀
      dom.removeEventListener(eventType, prevProps[name])
    })
  
  // 移除不再需要的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, newProps))
    .forEach(name => {
      dom[name] = ''
    })
  
  // 设置新的或更新的属性
  Object.keys(newProps)
    .filter(isProperty)
    .filter(isNew(prevProps, newProps))
    .forEach(name => {
      dom[name] = newProps[name]
    })
  
  // 添加新的事件监听器
  Object.keys(newProps)
    .filter(isEvent)
    .filter(isNew(prevProps, newProps))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2) // 去掉 'on' 前缀
      dom.addEventListener(eventType, newProps[name])
    })
}

讲解

  • createDom 根据 Fiber 节点的类型创建对应的 DOM 节点
  • updateDom 是一个高效的属性更新函数,只处理有变化的属性和事件
  • 先移除不再需要的事件和属性,再添加或更新新的事件和属性,避免不必要的 DOM 操作

七、Diff 算法:高效更新的核心

React 的 Diff 算法是其高效更新的关键,MiniReact 实现了核心的 Diff 逻辑

/**
 * 协调子元素 - React 的核心 Diff 算法
 * 比较新旧子元素,决定哪些需要更新、新增或删除
 */
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate?.child // 获取旧 Fiber 树的对应子节点
  let prevSibling = null // 用于构建兄弟节点链表
  
  // 同时遍历新元素和旧 Fiber 节点
  while (index < elements.length || oldFiber != null) {
    const element = elements[index] // 当前新元素
    let newFiber = null
    
    // 比较新旧元素的类型是否相同
    const sameType = element?.type == oldFiber?.type
    
    // 类型相同:复用 DOM 节点,只更新属性
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props, // 使用新的 props
        dom: oldFiber.dom, // 复用旧的 DOM 节点
        return: wipFiber, // 指向父节点
        alternate: oldFiber, // 指向旧节点,用于比较
        effectTag: "UPDATE", // 标记为更新操作
      }
    }
    
    // 有新元素但类型不同:需要创建新节点
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null, // 新节点,DOM 稍后创建
        return: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT", // 标记为新增操作
      }
    }
    
    // 有旧节点但类型不同:需要删除旧节点
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION" // 标记为删除操作
      deletions.push(oldFiber) // 加入删除队列
    }
    
    // 移动到下一个旧节点
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
    
    // 构建新的 Fiber 树结构
    if (index === 0) {
      wipFiber.child = newFiber // 第一个子节点
    } else if (element) {
      prevSibling.sibling = newFiber // 兄弟节点
    }
    
    prevSibling = newFiber
    index++
  }
}

讲解

  • React 的 Diff 算法采用同层比较策略,不跨层级比较,提高效率
  • 当元素类型相同时,复用 DOM 节点,只更新属性(UPDATE
  • 当有新元素且类型不同时,创建新节点(PLACEMENT
  • 当有旧节点且类型不同时,标记为删除(DELETION
  • 构建新的 Fiber 链表结构,维护 child 和 sibling 关系

八、模块导出与使用示例

1. 模块导出

/**
 * MiniReact 对象 - 暴露给外部使用的 API
 * 包含创建元素和渲染的核心功能
 */
const MiniReact = {
  createElement, // 用于 JSX 转换
  render // 渲染入口函数
}

// 将 MiniReact 挂载到全局对象,方便在浏览器中使用
window.MiniReact = MiniReact

2. 使用示例(index.jsx)

// 函数组件示例
function App(props) {
  return (
    <div className="app">
      <h1>Hello, {props.name}!</h1>
      <p>This is a MiniReact demo.</p>
    </div>
  );
}

// 渲染到页面
MiniReact.render(
  <App name="MiniReact" />,
  document.getElementById('root')
);

3. 页面引入(index.html)

<!DOCTYPE html>
<html>
<head>
  <title>MiniReact Demo</title>
</head>
<body>
  <div id="root"></div>
  <script src="./mini-react.js"></script>
  <script src="./index.js"></script>
</body>
</html>

九、总结:Fiber 架构的核心价值

MiniReact 用极简的代码实现了 React Fiber 架构的核心功能,包括:

  1. 虚拟 DOM:将 JSX 转换为可操作的数据结构

  2. Fiber 链表:将树结构转换为链表,支持中断与恢复

  3. 时间分片:利用浏览器空闲时间处理任务,避免卡顿

  4. 增量渲染:将渲染工作分解为小单元,逐步完成

  5. 高效 Diff:同层比较,最小化 DOM 操作

  6. 分阶段渲染:分为调度、协调和提交三个阶段

"撸过 MiniReact,React 源码再也不是天书!"

通过实现这个精简版的 React,我们可以清晰地看到:Fiber 架构的核心是将同步渲染改为可中断的异步渲染,通过时间分片提高应用的响应速度,这也是 React 高并发渲染的基础。