"如果你觉得 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 架构的核心功能,包括:
-
虚拟 DOM:将 JSX 转换为可操作的数据结构
-
Fiber 链表:将树结构转换为链表,支持中断与恢复
-
时间分片:利用浏览器空闲时间处理任务,避免卡顿
-
增量渲染:将渲染工作分解为小单元,逐步完成
-
高效 Diff:同层比较,最小化 DOM 操作
-
分阶段渲染:分为调度、协调和提交三个阶段
"撸过 MiniReact,React 源码再也不是天书!"
通过实现这个精简版的 React,我们可以清晰地看到:Fiber 架构的核心是将同步渲染改为可中断的异步渲染,通过时间分片提高应用的响应速度,这也是 React 高并发渲染的基础。