这是一个从零开始实现的 React 迷你版本,通过逐步提交的方式,带你深入理解 React 的核心原理。本项目实现了 React 的核心功能,包括:
- ✅ Fiber 架构
- ✅ 可中断的渲染调度器
- ✅ Virtual DOM 与 Diff 算法
- ✅ 函数组件
- ✅ Hooks(useState、useEffect)
- ✅ 事件处理和属性更新
- 第一步:初始化项目
- 第二步:实现调度器
- 第三步:分离渲染阶段和提交阶段
- 第四步:支持函数组件
- 第五步:重构函数组件
- 第六步:实现属性更新
- 第七步:重构属性更新
- 第八步:Diff 算法 - 类型不同时的处理
- 第九步:Diff 算法 - 删除多余节点
- 第十步:Diff 算法 - 边界情况
- 第十一步:优化更新流程
- 第十二步:实现 useState
- 第十三步:useState 支持 Action 队列
- 第十四步:优化 useState 避免冗余更新
- 第十五步:实现 useEffect
- 第十六步:useEffect 支持清理函数
Commit: a8511e9 - init
项目初始化,实现了最基础的 React 功能:
createElement:创建虚拟 DOMcreateTextNode:创建文本节点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)
}- JSX 转换:JSX 会被 Babel 转换为
createElement调用 - 虚拟 DOM:createElement 返回一个描述 DOM 结构的 JavaScript 对象
- 递归渲染: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)-
Fiber 数据结构:每个元素对应一个 Fiber 节点,包含:
child:第一个子节点sibling:下一个兄弟节点parent:父节点dom:对应的真实 DOM
-
工作循环:
workLoop在浏览器空闲时执行- 每次处理一个 Fiber 节点
- 如果时间不够,中断并等待下次空闲
-
遍历顺序:深度优先遍历
- 先访问子节点 (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)
}-
为什么要分离?
- 渲染阶段可能被中断,如果直接操作 DOM,用户会看到不完整的 UI
- 提交阶段一次性完成,保证 UI 的一致性
-
两阶段工作流程:
- 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)
}-
函数组件特点:
type是一个函数- 没有对应的 DOM 节点
- 通过调用函数获取子元素
-
处理差异:
- 函数组件:调用
fiber.type(fiber.props)获取子元素 - 主机组件:直接从
fiber.props.children获取子元素
- 函数组件:调用
-
提交阶段注意:
- 函数组件没有 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]
}
}
})
}-
effectTag:标记 Fiber 的操作类型
PLACEMENT:新增UPDATE:更新
-
alternate:指向上一次渲染的 Fiber
- 通过对比
alternate和当前 Fiber 实现 Diff
- 通过对比
-
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]
}
}
}
})
}三种属性变更情况:
- 旧有新无:删除属性
- 新有旧无:添加属性
- 新旧都有但值不同:更新属性
对于事件处理器,需要先移除旧的再添加新的,避免内存泄漏。
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)
}
}-
删除操作:
- 将要删除的 Fiber 收集到
deletions数组 - 在 Commit 阶段统一处理删除
- 将要删除的 Fiber 收集到
-
类型变化:
- 旧 Fiber 标记删除
- 创建新 Fiber 并标记为 PLACEMENT
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
}
}遍历完新的子元素后,如果还有剩余的旧子元素,说明它们需要被删除。
Commit: e0e8cfa - feat: diff, edge case
处理特殊情况:
- 子元素为
null或undefined - 子元素是数字类型
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)
}
}
// ...
})
}添加空值检查,确保不会为 null、undefined 创建 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)
}通过检查是否遍历完当前子树,避免不必要的更新,提升性能。
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]
}-
Hook 存储:
- 每个函数组件有自己的 Hook 数组(
stateHooks) - 使用索引(
stateHookIndex)区分多个 useState
- 每个函数组件有自己的 Hook 数组(
-
状态保持:
- 从
alternate.stateHooks获取上一次的状态 - 初次渲染时使用
initialValue
- 从
-
触发更新:
setState修改状态后,创建新的wipRoot- 触发新一轮渲染
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]
}-
Action 队列:
- 将每次
setState转换为 action 函数存入队列 - 下次渲染时统一执行队列中的所有 action
- 将每次
-
支持函数式更新:
setState(value):直接设置值setState(prev => prev + 1):基于前一个状态计算
-
批量更新:
- 多次
setState只触发一次渲染 - 确保状态更新的正确性
- 多次
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
}优化策略:
- 在触发更新前,先计算新值
- 如果新值和当前值相同,直接返回,不触发渲染
- 避免不必要的组件更新,提升性能
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)
}-
执行时机:
- 在 Commit 阶段完成后执行
- 确保 DOM 已经更新
-
依赖检查:
- 初次渲染:直接执行
- 更新时:对比依赖数组,有变化才执行
-
依赖对比:
- 使用浅比较(
!==)检查每个依赖项 - 任意一个依赖变化都会触发回调
- 使用浅比较(
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) // 再执行
}-
清理函数:
useEffect的回调可以返回一个清理函数- 清理函数在下次 effect 执行前调用
-
执行顺序:
1. 执行上一次的清理函数 2. 执行新的 effect 回调 3. 保存新的清理函数 -
特殊情况:
deps为空数组[]:只在首次渲染执行,清理函数在组件卸载时执行- 无
deps:每次渲染都执行
通过这 16 个提交,我们从零实现了一个 mini-react,涵盖了 React 的核心概念:
- Fiber 架构:可中断的渲染,提升用户体验
- 调度器:利用
requestIdleCallback在浏览器空闲时工作 - Diff 算法:高效对比虚拟 DOM,最小化 DOM 操作
- Hooks:
useState和useEffect的完整实现 - 两阶段提交:分离渲染和提交,保证 UI 一致性
- Virtual DOM:用 JavaScript 对象描述 UI
- Reconciliation:对比新旧虚拟 DOM,找出差异
- Fiber:React 的工作单元,支持增量渲染
- Effect:副作用管理,支持清理函数