-
执行pnpm install 安装简单的必要依赖。
-
执行npx tsc
npx tsc意味着:
-
执行 TypeScript 编译器
-
将 TypeScript (.ts/.tsx) 文件编译成 JavaScript
-
根据项目中的 tsconfig.json 配置进行编译
-
在没有全局安装 TypeScript 的情况下也能临时使用编译器
-
执行npx http-server .:
npx http-server . 意味着:
-
在当前目录启动一个简单的 HTTP 服务器
-
使当前目录的文件可以通过浏览器访问
-
解决直接打开 HTML 文件可能遇到的跨域问题
-
便于在本地测试 mini-react 应用
接下来配合启动的项目,开启React底层原理的学习之旅:
这是一个简化版 React 实现(Mini-React),目标是复制 React 的核心功能和工作原理,包括:
-
Fiber 架构实现异步可中断渲染
-
函数组件支持
-
Hooks API (useState, useEffect)
-
虚拟 DOM 和 DOM 操作
-
协调算法 (Reconciliation)
let nextUnitOfWork = null // 下一个工作单元,指向待处理的Fiber节点
let wipRoot = null // 当前正在构建的Fiber树根节点
let currentRoot = null // 上一次渲染完成的Fiber树根节点
let deletions = null // 需要删除的节点列表
let wipFiber = null // 当前正在处理的函数组件对应的Fiber节点
let stateHookIndex = null // 当前组件内Hook的索引
每个 Fiber 节点代表一个工作单元,包含以下属性:
{
type: string | function, // 元素类型:DOM标签名或函数组件
props: { // 元素属性
children: [], // 子元素数组
...otherProps // 其他属性和事件处理器
},
dom: HTMLElement | null, // 对应的真实DOM节点
return: Fiber, // 父Fiber节点
child: Fiber, // 第一个子Fiber节点
sibling: Fiber, // 下一个兄弟Fiber节点
alternate: Fiber, // 上一次渲染的对应Fiber节点
effectTag: string, // 操作标记:"PLACEMENT"|"UPDATE"|"DELETION"
stateHooks: [], // 状态钩子数组(函数组件)
effectHooks: [] // 副作用钩子数组(函数组件)
}
初始化渲染
↓
[render(element, container)]
↓
创建根 Fiber 节点 (wipRoot)
↓
设置 nextUnitOfWork = wipRoot
↓
[workLoop] ← requestIdleCallback
↓
处理当前 Fiber 节点 [performUnitOfWork]
↙ ↘
函数组件 普通元素
[updateFunctionComponent] [updateHostComponent]
↓ ↓
处理子元素 [reconcileChildren]
↓
返回下一个工作单元
↓
所有工作单元处理完毕
↓
[commitRoot] 提交阶段
↙ ↓ ↘
删除节点 更新节点 添加节点
↓
执行副作用 [commitEffectHooks]
↓
更新 currentRoot
↓
渲染完成
/**
* 渲染流程
* 初始化阶段:render函数创建第一个fiber节点(wipRoot),并设置nextUnitOfWork
* 任务调度:workLoop函数使用requestIdleCallback在浏览器空闲时执行渲染工作
* 构建Fiber树:performUnitOfWork函数根据当前fiber节点创建新的fiber节点,并设置nextUnitOfWork
* 提交阶段:commitRoot函数在任务完成后将Fiber树提交到DOM
*
* 使用Fiber树表示组件树,每个Fiber节点是一个工作单元
* 通过requestIdleCallback在浏览器空闲时执行渲染工作
* 采用深度优先遍历处理Fiber树
* 渲染分为两个阶段:
* Reconciliation/Render阶段:可中断,构建Fiber树
* Commit阶段:不可中断,将变更应用到DOM
*/
// React.render(<App />, document.getElementById('root'));
// 在这个调用中:
// <App /> 是根元素节点
// document.getElementById('root') 是容器节点
// 然后在 render 函数内部,这个根元素被包装到一个特殊的 Fiber 节点结构中:
function render(element, container) {
wipRoot = {
// 根节点
dom: container,
props: {
children: [element] // 初次渲染的时候,根元素被放在这里
},
alternate: currentRoot // 指向上一次渲染的树
}
deletions = []
nextUnitOfWork = wipRoot
}
-
创建根 Fiber 节点(wipRoot)
-
初始化 deletions 数组
-
设置 nextUnitOfWork 开始渲染过程
currentRoot 是指向当前已渲染到屏幕上的 Fiber 树根节点的引用,它在整个 Mini-React 实现中扮演着关键角色。
-
保存当前状态:currentRoot 保存着当前显示在屏幕上的 Fiber 树,它代表了应用的当前状态。
-
对比更新的基础:当状态发生变化需要重新渲染时,新的 Fiber 树(wipRoot)会通过 alternate 属性引用 currentRoot,从而能够对比新旧两棵树的差异。
-
实现协调算法:React 的核心是高效地确定哪些部分需要更新。通过比较 wipRoot 和 currentRoot,Mini-React 可以只更新真正变化的部分,而不是重建整个 DOM。
currentRoot 初始值为 null 的原因:
- 首次渲染:当应用首次渲染时,屏幕上还没有任何内容,所以没有"当前显示的 Fiber 树"。
- 区分初始渲染和更新:初始为 null 让我们可以区分是首次渲染还是更新操作。在 reconcileChildren 函数中,如果 wipFiber.alternate 为 null(即首次渲染),就不需要进行比较,直接创建新节点。
- 循环完成:首次渲染完成后,commitRoot 函数会将 wipRoot 赋值给 currentRoot,完成渲染循环:
currentRoot = wipRoot
wipRoot = null
初始渲染:
1. currentRoot = null
2. render() 创建 wipRoot
3. 构建并提交 Fiber 树
4. commitRoot() 设置 currentRoot = wipRoot
更新渲染:
1. 状态更新触发重新渲染
2. 创建新的 wipRoot,设置 wipRoot.alternate = currentRoot
3. 构建新 Fiber 树时通过 alternate 比较差异
4. commitRoot() 应用变更并更新 currentRoot
这种设计形成了一个完整的渲染循环,使得 Mini-React 能够高效地处理状态更新并反映到 UI 上,同时保持对 DOM 操作的最小化。
function workLoop(deadline) {
let shouldYield = false
// 循环处理工作单元,直到没有工作或需要让出控制权
while (nextUnitOfWork && !shouldYield) {
// 处理当前工作单元
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 检查剩余时间
shouldYield = deadline.timeRemaining() < 1
}
// 如果所有工作单元都处理完毕且有根节点,则提交根节点
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
-
使用 requestIdleCallback 在浏览器空闲时执行任务
-
循环处理工作单元直到时间用完或工作全部完成
-
当所有工作完成后(nextUnitOfWork 为 null),调用 commitRoot 提交更改
// 这是 Fiber 架构的核心函数,负责处理每个工作单元(Fiber 节点)并返回下一个要处理的工作单元。
function performUnitOfWork(fiber) {
// 判断是函数组件还是普通 DOM 元素
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 开始执行深度优先遍历:优先返回子节点
if (fiber.child) {
return fiber.child
}
// 没有子节点就找兄弟节点
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 没有兄弟节点就回到父节点
nextFiber = nextFiber.return
}
}
-
区分函数组件和普通 DOM 元素
-
执行相应的更新操作
-
实现深度优先遍历:优先处理子节点,然后是兄弟节点,最后回到父节点
function updateHostComponent(fiber) {
// 如果 DOM 还未创建,就创建它
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 协调子元素
reconcileChildren(fiber, fiber.props.children)
}
- 如果 DOM 不存在,创建 DOM 元素
- 协调子元素
// 这两个变量用于管理函数组件的状态和副作用。
let wipFiber = null // 当前正在工作的函数组件对应的 Fiber
/**
* function Counter() {
const [count, setCount] = useState(0); // hook索引0
const [name, setName] = useState("John"); // hook索引1
}
* 每次组件渲染时:
* 首先 stateHookIndex 被重置为 0
* 第一个 useState 调用使用索引 0,对应 count 状态
* 调用后 stateHookIndex++ 变为 1
* 第二个 useState 调用使用索引 1,对应 name 状态
* 以此类推
* 这就是为什么 React 要求 Hooks 必须在顶层使用,不能在条件语句中调用
* - 因为索引必须在每次渲染时保持一致,才能正确关联到对应的状态。
*/
let stateHookIndex = null // 当前组件内 Hook 的索引
/**
* 关键点:
* 函数组件本质上是一个函数,通过 fiber.type(fiber.props) 调用得到其返回的 React 元素
* 初始化数组来存储状态 Hooks 和副作用 Hooks
* 最后调用 reconcileChildren 处理子元素
*/
function updateFunctionComponent(fiber) {
// 设置当前工作中的函数组件 Fiber
wipFiber = fiber
// 重置 Hook 索引
stateHookIndex = 0
// 初始化状态 Hooks 和副作用 Hooks 数组
wipFiber.stateHooks = []
wipFiber.effectHooks = []
let children = []
// 防御性检查,确保 fiber.type 是函数
if (typeof fiber.type === 'function') {
// 调用函数组件得到子元素
children = [fiber.type(fiber.props)]
}
// 协调子元素
reconcileChildren(fiber, children)
}
-
设置当前工作 Fiber 为 wipFiber
-
重置 Hooks 索引和数组
-
执行函数组件获取其子元素
-
协调子元素
fiber.type(fiber.props) 是函数组件的执行过程,而 children = [fiber.type(fiber.props)] 将结果包装成数组。这是函数组件处理的核心机制,让我详细解释:
当 fiber.type 是一个函数时,这就表示当前处理的是一个函数组件。在 React 中,函数组件本质上就是一个接收 props 并返回 React 元素的函数。
// 函数组件示例
function MyComponent(props) {
return <div>Hello, {props.name}</div>;
}
在这个实现中:
-
fiber.type 就是函数组件本身(如 MyComponent 函数)
-
fiber.props 是传递给函数组件的属性
-
fiber.type(fiber.props) 就是执行这个函数,获取其返回的 React 元素
函数组件执行后返回的是单个 React 元素,但 reconcileChildren 函数需要处理子元素列表。将返回值包装成数组的原因是:
-
统一接口:reconcileChildren 函数期望接收一个元素数组作为参数
-
处理一致性:普通 DOM 元素的 children 本身就是数组,这样保持了处理逻辑的一致性
-
简化实现:即使函数组件只返回一个元素,将其放入数组可以用相同的循环逻辑处理
当处理一个函数组件(例如 )时:
-
fiber.type 是 Counter 函数本身
-
fiber.props 是传递给 Counter 的属性
-
执行 Counter(props) 得到返回的 React 元素(如
Count: 0) -
将这个返回值包装成数组 [
Count: 0] -
将这个数组传递给 reconcileChildren,后者会为这个返回的元素创建新的 Fiber 节点
这个机制使得函数组件能够:
-
根据传入的 props 动态生成元素结构
-
使用 hooks 管理状态和副作用
-
在每次状态变化时重新执行并生成新的元素树
这是 React 声明式编程模型的核心实现机制,让开发者可以通过编写描述"UI 应该是什么样子"的函数,而不必关心 DOM 操作的细节。
/**
* 当 performUnitOfWork 处理一个 Fiber 节点时,
* 会调用 updateFunctionComponent 或 updateHostComponent
*
* 这些函数会调用 reconcileChildren 来处理子元素
*
* reconcileChildren 会比较新旧元素,创建新的 Fiber 节点,并设置适当的 effectTag
*
* 当 workLoop 检测到所有工作单元处理完毕,会调用 commitRoot 来应用这些效果
*
* commitRoot 会根据 effectTag 执行相应的 DOM 操作
*
* wipFiber(work in progress Fiber):
* 来源:从 updateFunctionComponent 或 updateHostComponent 传入
* 作用:当前正在工作的 Fiber 节点,我们要为它创建或更新子节点
* elements:
* 来源:
* 对于函数组件:是函数组件执行结果 [fiber.type(fiber.props)]
* 对于普通元素:是 fiber.props.children
* 作用:新的子元素列表,需要与旧的 Fiber 节点进行比较
*/
function reconcileChildren(wipFiber, elements) {
// index:当前处理的子元素索引
let index = 0
/* oldFiber:
* 来源:通过 wipFiber.alternate?.child 获取,是上一次渲染的对应子节点
* 作用:用于与新元素比较
*/
let oldFiber = wipFiber.alternate?.child
// prevSibling:上一个处理过的兄弟节点,用于构建兄弟节点之间的链接
let prevSibling = null
// 函数使用一个 while 循环来处理所有子元素和所有旧的 Fiber 节点:
while (index < elements.length || oldFiber != null) {
const element = elements[index]
let newFiber = null
const sameType = element?.type == oldFiber?.type
/**
* effectTag 的作用
* effectTag 是一个标记,用于指示在 commit 阶段应该对这个 Fiber 节点执行什么操作:
* "UPDATE":更新现有的 DOM 节点
* "PLACEMENT":创建新的 DOM 节点并插入
* "DELETION":删除 DOM 节点
*/
// 如果新旧元素类型相同,则创建一个更新操作的 Fiber 节点
if (sameType && oldFiber && element) {
newFiber = {
type: oldFiber.type,
props: element.props, // 使用新的 props
dom: oldFiber.dom, // 复用旧的 DOM
return: wipFiber, // 指向父节点
alternate: oldFiber, // 指向旧节点
effectTag: "UPDATE" // 标记为更新
}
}
// 如果新元素存在且类型不同,则创建一个新元素的 Fiber 节点
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 的 child
if (index === 0) {
wipFiber.child = newFiber
}
// 如果当前不是第一个子元素,则设置为上一个兄弟节点的兄弟节点
else if (element) {
prevSibling.sibling = newFiber
}
// 更新上一个兄弟节点
prevSibling = newFiber
index++
}
}
- 对比新旧节点,决定更新操作
- 对相同类型节点执行更新(effectTag: "UPDATE")
- 对新增节点标记为放置(effectTag: "PLACEMENT")
- 对删除的节点添加到 deletions 数组(effectTag: "DELETION")
- 构建新的 Fiber 节点关系(父子、兄弟)
这是当前正在处理的 Fiber 节点,代表一个 DOM 元素。它包含了这个元素的所有信息,例如:
{
type: "div", // DOM 标签名
props: { ... }, // 元素属性
dom: (DOM引用), // 对应的真实 DOM 节点
return: (父Fiber), // 指向父 Fiber 节点
alternate: (旧Fiber), // 上一次渲染的对应节点
child: null, // 第一个子节点(尚未创建)
sibling: null // 下一个兄弟节点(尚未设置)
}
这是当前元素的所有子元素组成的数组,它们还只是虚拟 DOM 元素(React 元素),还没有转换为 Fiber 节点。
假设有以下 JSX 结构:
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>
当处理 div 元素时:
{
type: "div",
props: {
className: "container",
children: [
{ type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }] } },
{ type: "p", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "World", children: [] } }] } }
]
},
dom: (div DOM 引用),
return: (父 Fiber),
alternate: (旧 Fiber)
}
[
{ type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "Hello", children: [] } }] } },
{ type: "p", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "World", children: [] } }] } }
]
这个函数的作用是将子 React 元素转换为 Fiber 节点,并建立它们之间的关系:
-
对每个子元素创建或更新 Fiber 节点
-
设置父子关系 (return 指向父节点)
-
设置兄弟关系 (sibling 指向下一个兄弟节点)
-
标记更新类型 (effectTag: "PLACEMENT", "UPDATE" 或 "DELETION")
这样就完成了从虚拟 DOM 元素到 Fiber 节点的转换,构建了完整的 Fiber 树结构,为后续的 DOM 更新提供了基础。
if (oldFiber) {
oldFiber = oldFiber.sibling
}
这里不需要往子节点探索,因为reconcileChildren函数的目的是处理当前Fiber节点(wipFiber)的直接子元素,而不是递归处理整个树。
这段代码中oldFiber = oldFiber.sibling是在水平方向遍历兄弟节点,而不是往深度方向探索子节点。原因是:
-
reconcileChildren函数只负责一层子元素的协调工作,即当前Fiber节点的直接子元素
-
处理完一个子元素后,需要移动到下一个兄弟节点继续比较
-
深度遍历(子节点的子节点)是在后续的performUnitOfWork中完成的
整个Fiber树的构建是通过多次调用reconcileChildren实现的:
-
每次处理一个Fiber节点时,reconcileChildren只处理其直接子元素
-
当这些子元素被处理完并转化为新的Fiber节点后
-
performUnitOfWork会继续处理这些新创建的Fiber节点的子元素
这种设计允许React将渲染工作分解成小的工作单元,从而实现可中断的渲染过程。
/**
* 这个函数负责更新 DOM 元素的属性和事件处理器,分四步进行:
*
* 移除旧事件监听器:找出所有旧的事件属性,如果在新属性中不存在或值发生变化,则移除对应的事件监听器
* 删除旧属性:找出所有在新属性中不存在的普通属性,将它们设置为空字符串
* 设置新属性:找出所有值发生变化的普通属性,将它们设置到 DOM 元素上
* 添加新事件监听器:找出所有值发生变化的事件属性,为它们添加事件监听器
*
* 关键特点
* 属性处理的细粒度控制:区分普通属性和事件处理器,分别处理
* 高效更新:只更新发生变化的属性,避免不必要的 DOM 操作
* 事件处理:正确处理事件监听器的添加和移除,避免内存泄漏
* 函数式编程:使用函数式编程风格(过滤、映射等)处理属性
这部分代码实现了虚拟 DOM 到真实 DOM 的映射和更新,是实现声明式 UI 的核心机制。
*/
function updateDom(dom, prevProps, nextProps) {
// 移除旧的或变化的事件监听器
Object.keys(prevProps)
.filter(isEvent)
.filter(
key => !(key in nextProps) || isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
// 删除旧的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 设置或更新属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// 添加事件监听器
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
作用:更新DOM元素的属性和事件处理器
参数:
-
dom: 要更新的DOM元素
-
prevProps: 旧属性对象
-
nextProps: 新属性对象
核心逻辑:
-
移除旧的或变化的事件监听器
-
删除不再存在的属性
-
设置新的或变化的属性
-
添加新的事件监听器
Object.keys(prevProps)
.filter(isEvent) // 只处理事件属性(on开头的)
.filter(
key => !(key in nextProps) || isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
当事件处理器引用发生变化时(如从onClick={handler1}变为onClick={handler2}),浏览器的事件系统要求我们必须:
-
先移除旧的事件处理器
-
再添加新的事件处理器
这是因为DOM的addEventListener不会自动替换同类型的监听器,而是会累加。如果不先移除,就会导致多个处理器同时响应同一事件。
在代码的后续部分(没有展示),会有添加新事件处理器的逻辑:
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
/**
* Commit 阶段是 React 渲染过程的第二阶段,
* 负责将 Reconciliation 阶段生成的 Fiber 树应用到实际 DOM 上。
* 这是不可中断的同步过程。
*
* deletions:需要从 DOM 中移除的节点列表
* wipRoot:当前工作中的 Fiber 树根节点
* currentRoot:更新后保存的 Fiber 树,用于下次渲染比较
*/
function commitRoot() {
// 处理删除操作:先处理所有要删除的节点 deletions.forEach(commitWork)
deletions.forEach(commitWork)
// 递归提交子树:从根节点的第一个子节点开始 commitWork(wipRoot.child)
commitWork(wipRoot.child)
commitEffectHooks()
// 保存当前树:将完成的树保存为 currentRoot,用于下次渲染比较
// 当前渲染完成的树被保存为 currentRoot
currentRoot = wipRoot
// 清理状态:重置 wipRoot 和 deletions 数组
wipRoot = null
deletions = []
}
作用:提交阶段入口,将所有变更应用到DOM
重要操作:
-
处理需要删除的节点
-
递归提交整个Fiber树
-
执行副作用钩子
-
保存当前完成的树,用于下次渲染比较
-
重置相关状态
function commitWork(fiber) {
if (!fiber) {
return
}
// domParentFiber:寻找到的带有 DOM 引用的父级 Fiber 节点
let domParentFiber=fiber.return
// 不断向上找,直到找到可以挂载DOM的父级节点
while(!domParentFiber.dom){
domParentFiber=domParentFiber.return
}
// domParent:实际的父级 DOM 元素,用于添加/移除子节点
const domParent=domParentFiber.dom
// 按照增增删改的 effectTag 来分别做处理
/* domParent.appendChild(fiber.dom) 的作用是将 fiber.dom 作为子节点添加到 domParent 的子节点列表末尾。
这是一个"添加"操作,而非"替换"操作。 */
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)
}
作用:根据effectTag执行相应的DOM操作
参数:
- fiber: 当前处理的Fiber节点
核心逻辑:
-
查找有DOM引用的父节点
-
根据effectTag执行添加、更新或删除操作
-
递归处理子节点和兄弟节点
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
作用:执行DOM删除操作,支持函数组件
参数:
-
fiber: 要删除的Fiber节点
-
domParent: 父DOM元素
核心逻辑:
-
如果当前节点有DOM引用,直接从父元素中移除
-
如果没有DOM引用(函数组件),递归处理子节点
function useState(initialState){
// 获取当前函数组件对应的 Fiber 节点: const currentFiber = wipFiber
/**
* wipFiber: 当前正在处理的函数组件对应的 Fiber 节点
* 在 updateFunctionComponent 中设置
* 用于跟踪 Hooks 所属的组件上下文
*/
const currentFiber=wipFiber
// 尝试从上一次渲染中获取对应位置的 Hook
const oldHook=wipFiber.alternate?.stateHooks[stateHookIndex];
// 创建新的 Hook 对象,其状态值基于旧 Hook 或初始值:
const stateHook={
state: oldHook ? oldHook.state : initialState,
queue: oldHook ? oldHook.queue : [],
}
// 批量处理所有排队的状态更新操作:
/**
* 存储调用 setState 时传入的更新操作
* 下一次渲染时批量执行这些操作
*/
stateHook.queue.forEach((action) => {
stateHook.state = action(stateHook.state);
});
// 清空更新队列
stateHook.queue = [];
// 增加 Hook 索引
/**
* stateHookIndex: 当前组件内 Hook 的索引
* 每个组件内的 Hook 都有一个固定顺序
* 不能在条件语句中使用 Hook 的原因
*/
stateHookIndex++;
// 将 Hook 添加到当前组件的 Hooks 列表
wipFiber.stateHooks.push(stateHook);
function setState(action){
/**
* 支持两种更新方式:
* 直接传值: setState(newValue)
* 传递函数: setState(prevState => newState)
* 更新操作加入队列,但不立即执行
* 创建新的 wipRoot 并设置 nextUnitOfWork 触发重新渲染
* 通过 alternate: currentFiber 保存当前状态,以便下次渲染时对比
*/
const isFunction = typeof action === "function";
stateHook.queue.push(isFunction ? action : () => action);
/**
* 调用 setState 后,我们需要触发一次新的渲染
* 我们创建一个新的 wipRoot,基本上是当前 fiber 的复制
* 设置这个新 fiber 的 alternate 指向当前 fiber
* 这样在新的渲染过程中,新的 fiber 树可以通过 alternate 访问到当前状态
* 本质上,这是在创建一个新的 "工作中" fiber 树,它的 alternate 指向当前的 fiber 树。
* 这样在新的渲染循环中,我们能够对比新旧两棵树的差异。
*
* 这里的关系是:
* 新创建的 wipRoot:即将构建的新 fiber 树的根
* currentFiber:当前存在的 fiber 节点
* wipRoot.alternate = currentFiber:让新树能够引用到旧树
* 这种设计确保了状态更新时能够启动一个新的渲染过程,并且能够正确比较新旧状态。
*/
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextUnitOfWork = wipRoot;
}
return [stateHook.state, setState];
}
作用:实现React的useState钩子,提供状态管理能力
参数:
- initialState: 初始状态值
返回值:[状态值, 状态更新函数]
核心逻辑:
-
从上一次渲染中恢复状态,或使用初始值
-
批量处理所有排队的状态更新
-
增加Hook索引并保存Hook
-
提供setState函数,将更新操作入队并触发重新渲染
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
nextUnitOfWork = wipRoot
重新渲染的流程:
- 创建新的工作根节点:
-
wipRoot是"工作进行中的根节点"(work in progress root)
-
{...currentFiber}创建当前组件Fiber节点的浅拷贝
-
alternate: currentFiber建立新旧Fiber树之间的链接
- 设置下一个工作单元:
- nextUnitOfWork = wipRoot告诉渲染系统从根节点开始处理
- 触发渲染循环:
-
当nextUnitOfWork被设置后,正在运行的workLoop会检测到有新工作
-
workLoop会在浏览器空闲时开始处理这个新的工作单元
-
整个组件树会被重新构建,发现变更,最终提交到DOM
关键点是重新设置了全局变量nextUnitOfWork和wipRoot,这两个变量控制着渲染系统的工作状态。Mini-React的渲染系统通过不断检查nextUnitOfWork是否存在来决定是否需要继续渲染工作。
在前面的代码我们可以看到,requestIdleCallback(workLoop)已经被调用,使workLoop函数持续运行。当setState设置了nextUnitOfWork后,workLoop检测到这个变量并开始处理,从而启动新一轮渲染。
/**
* useEffect 函数
*
* 参数:
* callback: 回调函数,在组件挂载和更新时执行
* deps: 依赖数组,当依赖发生变化时,回调函数会被重新执行
*
*/
function useEffect(callback, deps) {
const effectHook = {
callback,
deps,
cleanup: undefined,
};
wipFiber.effectHooks.push(effectHook);
}
作用:实现React的useEffect钩子,处理副作用
参数:
-
callback: 副作用回调函数
-
deps: 依赖数组
核心逻辑:
-
创建effect钩子对象
-
将钩子添加到当前函数组件的effectHooks数组
/**
* useEffect 的工作原理
* 两个阶段执行:
* 先执行所有需要的清理函数
* 再执行新的副作用函数
* 依赖控制:
* 无依赖数组:每次更新都执行
* 空依赖数组([]):只在挂载和卸载时执行
* 有依赖:依赖变化时执行
*
* 清理机制:
* 副作用函数可以返回一个清理函数
* 在执行新的副作用前或组件卸载时调用清理函数
* 执行时机:
* 在 DOM 更新后异步执行
* 确保副作用能够访问到最新的 DOM
* 这种实现展示了 React 的 useEffect 的核心思想:通过依赖追踪管理副作用的执行,并在适当的时机执行清理工作。
*/
function commitEffectHooks(){
// 1. 定义执行清理函数的辅助函数
/**
* 流程和原理
* 递归终止条件:如果 fiber 不存在,直接返回
* 遍历旧 fiber 节点上的所有 effect hooks
* 获取对应新 fiber 节点上的依赖数组
* 如果依赖变化了,执行清理函数
* 递归处理子节点和兄弟节点
*
* 关键参数
* fiber.alternate:上一次渲染的 fiber 节点
* effectHooks:存储在 fiber 上的副作用钩子数组
* hook.cleanup:上一次执行副作用函数时返回的清理函数
* deps:依赖数组,决定何时重新执行副作用
*/
function runCleanup(fiber){
if(!fiber)return
fiber.alternate?.effectHooks?.forEach((hook,index)=>{
const deps=fiber.effectHooks[index].deps
/**
* 为什么在依赖变化时执行清理函数
* 防止资源泄露:
* 当依赖变化时,意味着副作用的上下文已经改变
* 如果不清理,可能会导致内存泄露、定时器冲突、网络请求堆积等问题
* 例如:如果 useEffect 中设置了定时器或订阅,不清理就会产生多个实例
*
* 确保副作用的一致性:
* 副作用应该与当前渲染的组件状态保持一致
* 当依赖变化,需要先"撤销"旧的副作用,再建立新的副作用
* 这样保证了在任何时刻,活跃的副作用都与当前状态匹配
*
* 避免副作用冲突:
* 不同依赖下的副作用可能有冲突行为
* 清理确保新的副作用在"干净"的环境中执行
*/
if(!hook.deps||!isDepsEqual(hook.deps,deps)){
hook.cleanup?.()
}
})
runCleanup(fiber.child);
runCleanup(fiber.sibling);
}
// 2. 定义执行副作用的辅助函数
/**
* 流程和原理
* 递归终止条件:如果 fiber 不存在,直接返回
* 遍历当前 fiber 节点上的所有 effect hooks
* 处理三种情况:
* 首次渲染:直接执行副作用函数
* 无依赖数组:每次更新都执行
* 有依赖数组:比较依赖,变化时执行
* 保存清理函数以便下次执行
* 递归处理子节点和兄弟节点
*
* 关键参数
* newHook:当前渲染中的 effect hook
* oldHook:上一次渲染中对应的 effect hook
* hook.callback:用户传入的副作用函数
* newHook.deps:当前的依赖数组
*/
function run(fiber) {
if(!fiber) return;
fiber.effectHooks?.forEach((newHook, index) => {
if(!fiber.alternate) {
newHook.cleanup = newHook.callback();
return;
}
if(!newHook.deps) {
newHook.cleanup = newHook.callback();
}
if(newHook.deps && newHook.deps.length > 0) {
const oldHook = fiber.alternate?.effectHooks?.[index];
// 添加安全检查
if(oldHook && oldHook.deps && !isDepsEqual(oldHook.deps, newHook.deps)) {
newHook.cleanup = newHook.callback();
}
}
});
run(fiber.child);
run(fiber.sibling);
}
// 3. 先执行清理函数
runCleanup(wipRoot);
// 4. 再执行新的副作用
run(wipRoot);
}
作用:执行副作用钩子的清理和回调函数
核心逻辑:
- 递归执行所有需要清理的副作用的清理函数
- 递归执行所有新的副作用函数
- 根据依赖变化决定是否执行
- render(element, container):创建根Fiber节点,设置nextUnitOfWork
- workLoop(deadline):通过requestIdleCallback调度,检查是否有工作和时间
- performUnitOfWork(fiber):处理当前Fiber节点,返回下一个工作单元
-
如果是函数组件,调用updateFunctionComponent(fiber)
-
如果是普通元素,调用updateHostComponent(fiber)
- updateFunctionComponent(fiber)/updateHostComponent(fiber):
-
处理当前节点(创建DOM或执行函数组件)
-
调用reconcileChildren(fiber, children)处理子元素
-
reconcileChildren(wipFiber, elements):比较协调子元素,创建子Fiber节点
-
当所有工作单元处理完毕,workLoop调用commitRoot()
-
commitRoot():递归执行所有DOM操作,调用commitEffectHooks()
- 用户调用setState(newState):
-
更新操作被加入队列
-
创建新的wipRoot,并设置nextUnitOfWork
-
同初次渲染的流程,但reconcileChildren会比较新旧节点
-
在commitRoot阶段,会根据effectTag执行相应的DOM更新
-
type & props:描述元素类型和属性
-
dom:与实际DOM的连接点
-
child, sibling, return:构成树状结构的链接
-
alternate:指向上一次渲染的对应节点,用于比较差异
-
effectTag:标记需要执行的DOM操作类型
-
nextUnitOfWork:指向下一个要处理的Fiber节点
-
wipRoot:当前正在构建的新Fiber树
-
currentRoot:上一次渲染完成的Fiber树
-
deletions:需要删除的节点列表
-
wipFiber & stateHookIndex:处理Hooks时的上下文
初始化
┌───────────────────────────────────────┐
│ createElement(type, props, ...children)│◄────── JSX 转换
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ render(element, container) │◄────── 应用入口
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 创建根Fiber节点: │
│ wipRoot = { │
│ dom: container, │
│ props: { children: [element] }, │
│ alternate: currentRoot │
│ } │
│ deletions = [] │
│ nextUnitOfWork = wipRoot │
└───────────────────┬───────────────────┘
▼
任务调度 ┌───────────────────────────────────────┐
┌───────────────►│ workLoop(deadline) │◄─┐
│ └───────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ 检查: nextUnitOfWork 存在 │ │
│ │ 且 !shouldYield │ │
│ └───────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ performUnitOfWork(nextUnitOfWork) │ │
│ └───────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ 检查: 是否所有工作单元处理完毕 │ │
│ │ 且wipRoot存在? │ │
│ └───────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ commitRoot() 执行提交 │ │
│ └───────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
└────────────────┤ requestIdleCallback(workLoop) │──┘
└───────────────────────────────────────┘
Fiber处理
┌───────────────────────────────────────┐
│ performUnitOfWork(fiber) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 判断: fiber.type instanceof Function │
└───────────────────┬───────────────────┘
▼
┌───────────────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 函数组件 │ │ 普通DOM元素 │
└────────┬────────┘ └────────┬────────┘
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│updateFunction │ │updateHost │
│Component(fiber) │ │Component(fiber) │
└────────┬────────┘ └────────┬────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ 创建DOM (如需要) │
│ └────────┬────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ reconcileChildren(fiber, children) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 深度优先遍历: │
│ 1. 返回child (如果存在) │
│ 2. 否则返回sibling (如果存在) │
│ 3. 否则回到parent找sibling │
└───────────────────────────────────────┘
Hooks处理
┌───────────────────────────────────────┐
│ useState(initialState) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 1. 从alternate获取oldHook (如果存在) │
│ 2. 创建新的stateHook对象 │
│ 3. 批量处理队列中的更新action │
│ 4. 返回[state, setState] │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ useEffect(callback, deps) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 1. 创建effectHook对象 │
│ 2. 添加到wipFiber.effectHooks │
└───────────────────────────────────────┘
提交阶段
┌───────────────────────────────────────┐
│ commitRoot() │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 1. 处理deletions中的节点 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 2. 递归处理整个fiber树 (commitWork) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 3. 执行effect hooks (commitEffectHooks)│
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 4. 保存当前树: currentRoot = wipRoot │
│ 重置: wipRoot = null, deletions = []│
└───────────────────────────────────────┘
- 增量渲染:通过Fiber架构将渲染工作拆分为小单元,实现可中断的渲染过程,提高应用响应性
- 双缓冲技术:使用wipRoot和currentRoot两棵树,一个用于构建新的UI状态,一个表示当前屏幕上的状态,实现高效更新
- 协调算法:通过比较新旧元素类型,高效计算最小变更集,减少DOM操作
- 单向数据流:状态更新通过setState触发重新渲染,确保数据流向清晰可预测
- 组合优于继承:使用函数组件和Hooks实现组合式UI构建,避免继承带来的复杂性
- 声明式UI:开发者描述UI应该是什么样子,而不是直接操作DOM
- 时间切片:利用浏览器空闲时间执行渲染工作,避免长时间阻塞主线程
这个Mini-React实现虽然简化了很多细节,但成功捕捉了React的核心工作原理。这个实现对于理解React内部机制非常有价值,尤其是对于理解React的Fiber架构、协调算法、Hooks机制等核心概念。
action是更新状态的指令,有两种形式:
-
直接值:setState(5)
-
更新函数:setState(prevState => prevState + 1)
代码会检查action类型:
const isFunction = typeof action === "function";
stateHook.queue.push(isFunction ? action : () => action);
如果是函数,直接使用;如果是值,将其包装成返回该值的函数。
wipRoot = {
...currentFiber,
alternate: currentFiber,
};
这不是创建两个相同的fiber节点,而是实现React的"双缓冲"技术:
-
创建一个新的"工作中"fiber树(wipRoot)
-
让它引用当前fiber树(currentRoot)作为比较基准
-
构建完成后,新树会替代旧树,成为下次更新的基准
这是为了确保状态更新能正确触发整个组件树的重新构建和比较。
函数组件本身不直接对应DOM节点。函数组件是一个返回React元素的函数。流程是:
- 执行函数组件得到React元素(虚拟DOM)
- 对这些元素调用reconcileChildren
- 如果元素是普通DOM元素,会在updateHostComponent中创建DOM
useState:
-
在updateFunctionComponent中,执行函数组件时会调用useState
-
useState从上一次渲染的stateHooks中读取状态
-
应用队列中的所有更新操作
-
组件渲染出新内容,reconcileChildren识别变化
useEffect:
-
在updateFunctionComponent中收集effect
-
在commitRoot阶段的commitEffectHooks中执行
-
先执行需要清理的旧effect的cleanup
-
再执行需要更新的effect
这是入口点设计:
-
容器元素(如div#root)成为根Fiber节点
-
应用的根组件(如)作为它的子元素
-
这构建了一个统一的起点,让整个应用可以从这个根开始递归处理
fiber.type存储元素的类型:
-
对于DOM元素:标签名字符串,如'div'、'span'
-
对于函数组件:组件函数本身,如function App() {...}
-
对于文本节点:特殊标识'TEXT_ELEMENT'
type用于:
-
确定如何处理节点(updateFunctionComponent或updateHostComponent)
-
创建正确类型的DOM元素
-
在协调阶段比较元素类型是否相同