1. 概述
本文是基于 react v19 而写的一篇 react 工作原理文档,后续将更新一篇 如何 0-1 实现 mini-react 的文,大家感兴趣的话,也可以期待一下。
1.1. 整体架构
四大核心包的职责:
| 包名 | 路径 | 职责 |
|---|---|---|
| react | packages/react | 定义组件 API、Hooks、createElement、JSX Runtime |
| react-reconciler | packages/react-reconciler | Fiber 架构核心,Diff 算法,协调更新 |
| react-domn | packages/react-dom | DOM 渲染器,事件系统,浏览器宿主操作 |
| scheduler | packages/scheduler | 任务优先级调度,时间切片 |
1.1.1. 核心数据结构:fiber
Fiber 是 React 的核心数据结构,每个 React 元素对应一个 fiber 节点,fiber 的数据结构如下:
FiberNode {
// 身份标识
tag: WorkTag, // 组件类型 (FunctionComponent=0, ClassComponent=1, HostComponent=5...)
key: null | string,
elementType: any, // createElement 的第一个参数
type: any, // 解析后的类型 (function/class)
stateNode: any, // 指向真实实例
// Fiber 树结构 (链表树)
return: Fiber | null, // 父节点
child: Fiber | null, // 第一个子节点
sibling: Fiber | null, // 下一个兄弟节点
index: number,
// 状态
pendingProps: any, // 新的 props
memoizedProps: any, // 上次渲染的 props
memoizedState: any, // 上次渲染的 state (Hooks 链表头)
updateQueue: mixed, // effect副作用的更新队列
// 副作用
flags: Flags, // 副作用标记 (Placement, Update, Deletion...)
subtreeFlags: Flags, // 子树副作用标记 (冒泡优化)
// 双缓冲
alternate: Fiber | null, // 指向另一棵树的对应节点 (current ↔ workInProgress)
// 优先级
lanes: Lanes, // 本节点的优先级,也用于标记fiber节点是否有更新
childLanes: Lanes, // 子树的优先级,也用于标记子树fiber节点是否有更新
}
1.1.2. fiber 树遍历
- fiber 本质上是一颗链表树(左孩子右兄弟链表编码的树),逻辑结构是树,物理实现是链表,遍历方式是链表的遍历,不采用递归,所以可以随时暂停和恢复(ps: 递归之所以不可以暂停,是因为调用栈保存在 js 引擎中,无法在 js 层面保存和恢复这个调用栈)
- fiber 树的遍历采用深度优先遍历,每一步都可中断,例子如下:
<App>
<Header />
<Main>
<Article />
<Sidebar />
</Main>
<Footer />
</App>
对应的fiber链表树:
App
│ child
▼
Header ──sibling──▶ Main ──sibling──▶ Footer
│ child
▼
Article ──sibling──▶ Sidebar
所有节点的 return 指针:
Header.return = App
Main.return = App
Footer.return = App
Article.return = Main
Sidebar.return = Main
遍历顺序 (performUnitOfWork):
1. beginWork(App) → 返回 child = Header
2. beginWork(Header) → 返回 null (无子节点)
3. completeWork(Header) → 有 sibling = Main
4. beginWork(Main) → 返回 child = Article
5. beginWork(Article) → 返回 null
6. completeWork(Article)→ 有 sibling = Sidebar
7. beginWork(Sidebar) → 返回 null
8. completeWork(Sidebar)→ 无 sibling, return 到 Main
9. completeWork(Main) → 有 sibling = Footer
10. beginWork(Footer) → 返回 null
11. completeWork(Footer)→ 无 sibling, return 到 App
12. completeWork(App) → 完成
这就是 Fiber "可中断递归" 的基础 — 任何一步都可以暂停,因为当前处理到哪个节点、下一步该去哪里,都由 child/sibling/return 指针决定,不依赖 js 调用栈。
2. 核心流程
2.1. 总体流程
用户操作 (click/input/...)
│
▼
① 触发更新 (Trigger)
setState() / dispatch()
│
▼
② 创建 Update 对象 { action, lane, next }
│
├─ 挂到对应 hook.queue.pending(环形链表)
│ (dispatch 通过闭包绑定了具体的 fiber 和 queue,不会挂错)
▼
③ 标记 Lane 优先级,从触发节点向上冒泡到 FiberRootNode
│
▼
④ 调用 ensureRootIsScheduled() 进入调度器
│
▼
⑤ Scheduler 根据优先级调度回调
│
│ SyncLane → MicroTask 最高优先级 performSyncWorkOnRoot 比如点击/输入触发的更新
│ DefaultLane → MessageChannel 默认优先级 performConcurrentWorkOnRoot
│ 宏任务不用setTimeout实现,有延时,messageChanel可以做到0延迟宏任务
│ TransitionLane → 低优先级 performConcurrentWorkOnRoot
│
▼
⑥ ═══════ Render 阶段 (可中断) ═══════ (在内存中构建一颗新的fiber树,标记所有需要的dom变更)
│
│ performSyncWorkOnRoot() / performConcurrentWorkOnRoot()
│ │
│ ▼
│ 根据lanes,判断是 重新创建workInProgress树,还是 断点续传
│ prepareFreshStack() → 创建workInProgress树首节点
│ │
│ ▼
│ workLoopSync() / workLoopConcurrent() (深度优先遍历)
│ │
│ │ performUnitOfWork(workInProgress); 工作单元
│ │
│ ├──▶ beginWork(current, wip, lanes) ← 递 (向下)
│ │ │ bailout,判断节点是否需要跳过
│ │ ├─ 根据 fiber.tag 分发处理
│ │ │ ├─ FunctionComponent -> renderWithHooks
│ │ │ ├─ ClassComponent │
│ │ │ ├─ HostComponent ▼
│ │ │ └─ ...
│ │ │
│ │ ├─ Diff 算法 (reconcileChildFibers)(diff当前fiber的直接子级)
│ │ │ ├─ 单节点 diff
│ │ │ └─ 多节点 diff (两轮遍历)
│ │ │
│ │ └─ 返回 child fiber 或 null
│ │
│ └──▶ completeWork(current, wip, lanes) ← 归 (向上)
│ │
│ ├─ 创建/更新 DOM 实例 (不挂载)
│ ├─ flags 冒泡
│ └─ 沿 return 指针向上
│
│ 完成后得到一棵带 flags 标记的 workInProgress Fiber 树
│
▼
⑦ ═══════ Commit 阶段 (同步不可中断) ═══════
│
│ commitRoot(root)
│ │
│ ├─── Before Mutation 阶段
│ │
│ ├─── Mutation 阶段 (操作真实 DOM)
│ │
│ ├─── 切换 current 指针: root.current = finishedWork
│ │
│ └─── Layout 阶段
│
▼
⑧ 浏览器绘制 (Paint)
│
▼
⑨ useEffect 异步执行
2.2. 触发阶段
2.2.1. 创建 Update 对象
Update 对象就是一个记录"要做什么改变"的数据包,挂到对应 hook 的 queue 上,数据结构如下:
// Update 对象
const update = {
lane: lane, // 这个更新的优先级
action: action, // 你传的值:1 或 (prev) => prev + 1
hasEagerState: false, // 是否有提前计算的结果
eagerState: null, // 提前计算的结果
next: null, // 环形链表指针
};
其中queue为环形链表,假如setCount了三次,那么结构分别如下:
setCount(1):
queue.pending → A{action:1} ↺
setCount(2):
queue.pending → B{action:2} → A{action:1} → B ↺
setCount(3):
queue.pending → C{action:3} → A{action:1} → B{action:2} → C ↺
↑ │
└───────────────────────────────────────────┘
pending 指向最后插入的 C
C.next 指向第一个 A
render阶段消费时,从 pending.next 开始 → A → B → C(插入顺序)
2.2.2. lanes 冒泡
标记 lane 就是在 fiber 上记录"我有这个优先级的更新要处理",冒泡标记父级 fiber 的childLanes就是告诉父节点 fiber "我下面有更新"。
scheduleUpdateOnFiber(fiber, lane)
│
├─ 标记当前 fiber
│ fiber.lanes = fiber.lanes | SyncLane
│ (位运算合并,一个 fiber 可以同时有多个 lane)
│
└─ 冒泡:沿 return 指针往上走,标记每个祖先的 childLanes
let parent = fiber.return
while (parent !== null) {
parent.childLanes = parent.childLanes | SyncLane
parent = parent.return
}
举个例子:
setCount(1) 在 Counter 组件触发,lane = SyncLane
标记前:
HostRoot { lanes: 0, childLanes: 0 }
App { lanes: 0, childLanes: 0 }
Counter { lanes: 0, childLanes: 0 } ← setState 发生在这里
div { lanes: 0, childLanes: 0 }
标记后:
HostRoot { lanes: 0, childLanes: SyncLane } ← 冒泡到这里
App { lanes: 0, childLanes: SyncLane } ← 冒泡到这里
Counter { lanes: SyncLane, childLanes: 0 } ← 标记自己
div { lanes: 0, childLanes: 0 }
冒泡的目的: 为了给 render 阶段 workLoop 工作循环的 beginWork 指路,判断是否子树有更新
render 阶段 workLoop 从 HostRoot 开始往下走:
beginWork(HostRoot)
自己 lanes = 0 → 不需要处理自身
childLanes 有 SyncLane → 子树有活 → 往下走 ✓
beginWork(App)
自己 lanes = 0 → bailout(不执行 App 函数)
childLanes 有 SyncLane → 子树有活 → 继续往下 ✓
beginWork(Counter)
自己 lanes 有 SyncLane → 有更新!→ 执行 Counter() ✓
beginWork(div)
自己 lanes = 0,childLanes = 0 → 全跳过 ✓
2.3. scheduler 调度
理论上任何可能产生更新的地方都会调用ensureRootIsScheduled
setState / forceUpdate
└─ scheduleUpdateOnFiber
└─ ensureRootIsScheduled ← 每次状态更新后
commitRoot(提交完成后)
└─ ensureRootIsScheduled ← 检查是否还有剩余工作
flushPassiveEffects(useEffect 执行后)
└─ ensureRootIsScheduled ← effect 中可能产生了新更新
ensureRootIsScheduled(root)
│
├─ ① 已经派过同优先级的活了吗?
└─ 是 → 不重复派,return(这就是自动批处理的基础)
用户连续点击 setState 三次(同一事件中):
setCount(1) → ensureRoot → 没有已有调度 → 派活(微任务)
setCount(2) → ensureRoot → 已有同优先级 → return ✓
setCount(3) → ensureRoot → 已有同优先级 → return ✓
事件结束 → 微任务执行 → 一次性渲染(三个 update 一起消费)
└─ 否 →
transition 渲染中(低优 TransitionLane) → 用户点击(高优 SyncLane)
ensureRoot 发现:
已有调度 = TransitionLane
新优先级 = SyncLane
不同!→ 取消旧的任务 → 派新的高优任务
(丢弃旧wip树是在render阶段开始时执行)
└─ ② 怎么派?
├─ SyncLane → MicroTask → performSyncWorkOnRoot(root) 点击/输入/flushsync
└─ 其他 Lane → MessageChannel → performConcurrentWorkOnRoot(root) useEffect
两个入口最终都进入 render:
performSyncWorkOnRoot(root) performConcurrentWorkOnRoot(root)
├─ 不可中断 ├─ 可中断
├─ renderRootSync(root, lanes) ├─ renderRootConcurrent(root, lanes)
│ ├─ prepareFreshStack() │ ├─ prepareFreshStack()
│ └─ workLoopSync() │ └─ workLoopConcurrent()
│ │
└─ commitRoot(root) ├─ 检查结果:
│ ├─ RootCompleted → commitRoot(root)
│ └─ RootInProgress → 等下次调度继续
└─ (可能被更高优先级中断)
2.4. render 阶段
2.4.1. 重新创建 wip 树 / 断点续传
每次 render 开始前,会先判断 创建新的 wip 树 / 旧 wip 树断点续传,prepareFreshStack 函数用于创建 wip 树的首节点 HostRoot fiber。
renderRootSync(root, lanes) / renderRootConcurrent(root, lanes)
│
├─ 当前是否有进行中的 wip 树?
│ ├─ 没有 → prepareFreshStack (新建 wip 树)
│ └─ 有进行中的 wip 树:
│ ├─ 本次任务 lanes !== 正在进行的 lanes? (例如高优插队)
│ │ └─ 是 → prepareFreshStack (丢弃旧树,重新开始)
│ │
│ └─ 本次任务 lanes === 正在进行的 lanes? (断点续传)
│ └─ 跳过 prepareFreshStack,直接继续 workLoop
│ │
│ └─ React19 核心改变(react18采用双队列来交错更新,复杂且极端场景容易出bug)
│ 如果此时触发了新的同优先级 setState:
│ 1. React 会把新 update 暂存,暂不干扰当前 wip 树。
│ 2. workLoop 继续跑完当前的半成品树,直到渲染完成。
│ 3. 渲染循环结束后,调用 finishQueueingConcurrentUpdates,把暂存的 update 正式挂到 Fiber 树上。
│ 4. 在 commit 阶段后,由 ensureRootIsScheduled 为这些遗留的 update 再次开启一轮全新的渲染。
2.4.2. 工作循环(workLoopSync / workLoopConcurrent)
// 同步模式: 点击、输入、flushSync等走这条路
function workLoopSync() {
// 没有任何判断,一口气干完,创建好本次更新后的完整wip树
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 并发模式: 每个单元后检查时间
function workLoopConcurrent() {
// 每处理一个 fiber,就检查一次:时间片用完了吗?
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
// 处理fiber的工作单元
function performUnitOfWork(unitOfWork: Fiber) {
const current = unitOfWork.alternate;
// 递:处理当前节点,返回它的第一个child
const next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next !== null) {
// 有 child → 继续往下走
workInProgress = next;
} else {
// 没 child → 到底了,开始往上走
completeUnitOfWork(unitOfWork);
}
}
function completeUnitOfWork(unitOfWork: Fiber) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 归:创建/更新 真实DOM节点,flags冒泡
completeWork(current, completedWork, renderLanes);
// 有兄弟?→ 兄弟成为下一个 workInProgress,退出归阶段
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return; // ← 回到 workLoopSync,对 sibling 开始新一轮"递"
}
// 没兄弟?→ 继续往上归
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
2.4.3. 递阶段和归阶段(beginWork 和 compeleWork)
2.4.3.1. beginWork
2.4.3.1.1. bailout 判断
判断是否可以跳过,需要满足三个条件才可以跳过
beginWork(current, workInProgress, renderLanes)
│
│ ═══ 第一步: 判断是否可以跳过 ═══
│
├─ current !== null? (有旧 fiber = update 阶段)
│ │
│ │ const oldProps = current.memoizedProps;
│ │ const newProps = workInProgress.pendingProps;
│ │
│ │ 三个条件全部满足才可能 bailout:
│ │
│ │ ① oldProps === newProps (props 引用没变)
│ │ ② !hasLegacyContextChanged() (旧版 context 没变)
│ │ ③ !includesSomeLane( (本节点没有待处理的更新)
│ │ workInProgress.lanes, renderLanes)
│ │ lanes用来标记有没有更新,是什么优先级,具体的更新内容在更新队列上
│ │
│ ├─ 全满足
│ │ │
│ │ └─ bailoutOnAlreadyFinishedWork(current, wip, renderLanes)
│ │ │
│ │ ├─ wip.childLanes 子树是否有更新
│ │ │ ├─ YES → cloneChildFibers(current, wip)
│ │ │ │ return wip.child
│ │ │ │ ↑ 本节点跳过,但子树有更新,克隆后继续向下
│ │ │ │
│ │ │ └─ NO → return null
│ │ │ ★ 整棵子树全部跳过!★
│ │ │ 这是 React 最强力的性能优化
│ │ │
│ │ └─ cloneChildFibers 做什么:
│ │ 不重新 reconcile,而是直接复制
│ │
│ └─ 不全满足 → didReceiveUpdate = true → 进入完整处理
│
├─ current === null? (首次渲染,无旧 fiber)
│ → 直接进入完整处理 (不可能 bailout)
2.4.3.1.2. 按 tag 分发处理
│ ═══ 第二步: 根据 tag 执行具体逻辑 ═══
│
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
const nextProps = workInProgress.pendingProps;
return updateFunctionComponent(
current, workInProgress, Component, nextProps, renderLanes
);
}
case ClassComponent: { ... }
case HostComponent: { ... }
...
}
// 函数组件处理流程
updateFunctionComponent(current, wip, Component, {}, renderLanes)
│
├─ renderWithHooks:
│ ├─ 重置 wip.memoizedState / updateQueue / lanes
│ │ // hook 链表清空 effect 链表清空
│ ├─ 选择 dispatcher 首次渲染 和 更新渲染的行为完全不同
│ └─ Component(props) -> 执行组件函数,hooks运行,返回children
│
└─ reconcileChildFibers(returnFiber, currentFirstChild, newChild) // diff过程
│
├─ newChild 是单个元素(object, $$typeof === REACT_ELEMENT_TYPE)
│ → reconcileSingleElement()
│ ├─ 遍历旧 children,找 key 和 type 都匹配的
│ │ ├─ 找到 → useFiber() 复用旧 Fiber,更新 props,删除其余 sibling
│ │ └─ 没找到 → createFiberFromElement() 创建新 Fiber
│ └─ 多余的旧节点 → 标记 Deletion
│
├─ newChild 是数组(多个子元素)
│ → reconcileChildrenArray()
│ ├─ 第一轮:从左到右逐个比较(fiber更新的频率最多,第一轮主要处理要更新的节点)
│ │ ├─ key 相同 + type 相同 → 复用,继续
│ │ ├─ key 相同 + type 不同 → 删旧建新,继续
│ │ └─ key 不同 → 停下来,进入边界判断/第二轮
│ ├─ 旧的遍历完了? → 剩下的新元素全部 create,标记 Placement
│ ├─ 新的遍历完了? → 剩下的旧 Fiber 全部标记 Deletion
│ └─ 第二轮:把剩余旧节点放入 Map<key, Fiber>
│ → 遍历剩余新元素,从 Map 中找可复用的
│ → 找到 → 复用,oldIndex < lastPlacedIndex 才标记移动
│ → 没找到 → 新建,标记 Placement
│ → Map 中剩余的 → 全部标记 Deletion
│
├─ newChild 是文本/数字
│ → reconcileSingleTextNode()
│
└─ newChild 是 null/undefined
→ deleteRemainingChildren()
2.4.3.1.3. renderWithHooks
- 重置 hooks/副作用/lanes 状态
- React 的 hooks 是一条单向链表,挂在 fiber.memoizedState 上。每次执行组件函数时,每个 useState / useEffect 调用都会创建一个新的 hook 节点,按调用顺序串成新链表。
- 重置是为了从头开始构建这条新 hooks 链表。
current fiber (旧树) wip fiber (新树)
memoizedState memoizedState
↓ ↓
[hook0] → [hook1] → [hook2] [hook0] → [hook1] → [hook2]
↑ ↑
currentHook (读旧值) workInProgressHook (写新值)
在执行函数组件时,每次调用一个 hook(比如 useState),都会创建新的 hook 节点,把旧状态拷过来,再应用 pending 的 update queue,把新 hook 挂到 wip.memoizedState 链表上
即:
链表结构 状态值
Mount 从零创建 用 initialState
Update 从零重建 从currentHook.memoizedState 读取 + 处理 queue 更新队列
- 选择 dispatcher
- 首次渲染和更新渲染的行为是不同的,需要采用不同的 dispatcher 去处理
// 首次渲染(Mount)
HooksDispatcherOnMount = {
useState: mountState, // 创建 hook 节点,初始化 state
useEffect: mountEffect, // 创建 effect,标记需要执行
useRef: mountRef, // 创建 { current: initialValue }
useMemo: mountMemo, // 计算并缓存
...
};
// 更新渲染(Update)
HooksDispatcherOnUpdate = {
useState: updateState, // 读取 hook 节点,处理 queue.pending 环形链表
useEffect: updateEffect, // 对比依赖,决定是否重新执行
useRef: updateRef, // 直接返回已有的 ref
useMemo: updateMemo, // 对比依赖,决定是否重新计算
...
};
- 执行函数组件,重建 hooks 链表,返回 children
自上而下运行函数组件
1. useState
a. mount → 创建 Hook,初始化 state,绑定 dispatch
b. update → 取出对应 Hook,消费 queue.pending 中的 Update 链表,算出新 state
2. useEffect
a. mount → 创建 Hook,创建 Effect { create, destroy, deps, tag: HookHasEffect }
b. update → 取出对应 Hook,对比 deps
1. 变了 → Effect.tag 带 HookHasEffect(要执行)
2. 没变 → Effect.tag 不带(跳过)
3. hook.memoizedState = effect
4. pushEffect → 追加到 fiber.updateQueue 环形链表
5. fiber.flags |= Passive
3. 返回 children(ReactElement 对象树)
组件代码: hooks 链表 (memoizedState):
const [count, setCount] hook0: { memoizedState: 0, queue: {}, next: → }
= useState(0);
useEffect(() => { hook1: { memoizedState: effectObj, next: null }
document.title = count;
}, [count]);
return (
<button onClick={() => {
setCount(1); 待更新update
}}>
{count}
</button>
);
Fiber (<Componnet />) // 用户点击了一次按钮 setCount(1),从触发到render阶段hooks变化
│
│ fiber的memoizedState存储的是hooks链表的头节点
│ state hook的更新操作存储在对应的 hook.queue 里面
│ effect 分别存储在对应的 hook.memoizedState 和 fiber.updateQueue 里
│
└── memoizedState
──► Hook 0 (useState)
// trigger阶段
memoizedState: 0 ← state 值
queue: {
// 在trigger阶段产生Update对象追加在此
pending ──► Update { action: 1, lane: SyncLane }
}
// ——> render阶段,即执行组件函数 Component(props) ,重建hooks链表时候
memoizedState: 1 ← state 新值
queue: {
pending ──► null
}
next ──► Hook 1 (useEffect)
memoizedState ──► Effect {
// trigger阶段 ──► render阶段
tag: HookHasEffect ──► tag: HookHasEffect
deps: [0] ──► deps: [1] ✓ 新 deps
destroy: undefined ──► destroy: fn ✓ 上轮返回的清理函数
}
next ──► null
└── updateQueue──► render阶段不消费,重新执行一遍组件函数,useEffect就会重新pushEffect
├── memoizedState: effect 对象
└── next ──► null
// 时间线变化
Mount 后 Trigger 后 Render 后(wip)
──────── ────────── ─────────────
hook.queue
.pending null Update{1} ↺ null(已消费)
fiber
.updateQueue Effect(deps:[0]) Effect(deps:[0]) Effect(deps:[1])
↑ ↑ ↑
mount 时创建 没动过 清空后重建
注意⚠️:点击setCount一次,那么创建的update对象,是如果知道自己应该挂载在哪个hook的queue里呢?
答案:是闭包,setCount在创建时就已经记住了在哪个fiber的哪个hook节点里。
注意⚠️: useEffect的存储位置为两个
| 存储位置 | 谁用 | 用来干嘛 |
|---|---|---|
| Hook 链表 (fiber.memoizedState) | render 阶段 | 按顺序匹配 hook,对比 deps 决定这轮 effect 要不要重新执行 |
| fiber.updateQueue | commit 阶段 | 快速遍历所有需要执行的 effect,不用再走一遍 hook 链表 |
reconcileChildFibers
diff 过程,为新的 ReactElement 和旧的 Fiber 的对比,此时这里仅 diff 当前 fiber 的直接子级。
1.新的 ReactElement 和旧的 Fiber 的对比:
新(组件函数刚执行返回的) 旧(current 树上已有的)
───────────────────── ─────────────────
ReactElement Fiber
{ type: 'span', { type: 'span',
props: { children: 1 }, memoizedProps: { children: 0 },
key: null } key: null }
│ │
└──────── 比较 type 和 key ─────────┘
│
相同 → 复用 Fiber,用新 props 更新
不同 → ...
2.仅 diff 当前 fiber 的直接子级
<App> ← beginWork(App) 时 diff 这一层的 children
<Header /> ↑ 只对比 App 的直接子级
<Main> ↑ Header、Main、Footer
<Article /> ✗ 不管,等 beginWork(Main) 时再 diff
<Sidebar /> ✗ 不管
</Main>
<Footer /> ↑
</App>
而逐层 diff,靠 workLoop 工作循环来推进,举个例子:
function App() {
const [count, setCount] = useState(1);
return (
<div>
<span>{count}</span>
<button>+1</button>
</div>
);
}
假设 count 从 0 变成 1:
beginWork(App)
执行 App() → 返回 Element { type: 'div', children: [span, button] }
reconcile: 旧 div fiber vs 新 div element → type和key相同 → 复用,更新 props
返回 wip div fiber ↓
beginWork(div)
HostComponent,不执行组件函数
从 pendingProps.children 取出 [span element, button element]
reconcile 多节点 diff:
span fiber vs span element → key和type相同 → 复用, props 变了(children: 0→1)
button fiber vs button element → key和type相同 → 复用, props 没变
返回 wip span fiber ↓
beginWork(span)
HostComponent,pendingProps.children = 1(新)vs memoizedProps.children = 0(旧)
文本变了 → fiber.flags |= Update
返回 null → completeUnitOfWork 开始 ↑
每次 beginWork 只 diff 当前 fiber 的直接子级(一层),
整棵树的 diff 是 workLoop 逐层推进、多次 beginWork 累积的结果。
2.4.3.2. completeWork
completeWork 是归阶段的核心,主要是创建/更新真实 dom 节点(但不挂载到页面上去),收集 flags 向上冒泡。
2.4.3.2.1. 创建/更新 dom 节点
completeWork(current, workInProgress, renderLanes)
│
├─ HostComponent(div, span 等原生标签)
│ │
│ ├─ mount(current === null):
│ │ ├── document.createElement('div') ← 创建真实 DOM
│ │ ├── 把所有子 DOM 节点 append 进去 ← 组装 DOM 树
│ │ ├── 设置 props(className, style, 事件等)
│ │ └── fiber.stateNode = dom ← 存到 fiber 上
│ │
│ └─ update(current !== null):
│ ├── DOM 已存在(fiber.stateNode 有值)
│ ├── 对比 oldProps vs newProps
│ │ ├─有变化 → 收集到 fiber.updateQueue = ['className', 'new-class', ...]
│ │ │ fiber.flags |= Update
│// HostComponent(div, span 等)的 updateQueue 与 FunctionComponnets 存储内容不同
│ │ └─ 没变化 → 什么都不做
│ └── 不创建新 DOM,不操作 DOM(等 commit 阶段)
│
├─ HostText(文本节点)
│ ├─ mount → document.createTextNode(text)
│ └─ update → 标记 Update(commit 时改 nodeValue)
│
├─ FunctionComponent
│ └─ 基本什么都不做(没有 DOM 要创建)
│
└─ 其他 tag → ...
2.4.3.2.2. Flags 冒泡
Flags 冒泡就是:每个节点把自己和子孙的 flags 汇总报告给父节点,目的只有一个:让 commit 阶段快速跳过没有变化的子树。
注意⚠️:lanes 冒泡是方便 render 阶段的 beginWork 跳过子树,Flags 冒泡是方便 commit 阶段跳过子树。
<App> // flags: Passive(有 useEffect)
<div> // flags: 0 // 无变化
<span>1</span> // flags: Update(文本变了)
<b>hello</b> // flags: 0
</div>
<Footer /> // flags: 0
</App>
“归”阶段:
complete span → flags: Update, subtreeFlags: 0
complete b → flags: 0, subtreeFlags: 0
complete div → flags: 0
subtreeFlags: span.flags | b.flags | span.subtreeFlags | b.subtreeFlags
= Update | 0 | 0 | 0
= Update
↑ div 自己没变化,但它知道子树里有变化
complete Footer → flags: 0, subtreeFlags: 0
complete App → flags: Passive
subtreeFlags: div.flags | Footer.flags | div.subtreeFlags | Footer.subtreeFlags
= 0 | 0 | Update | 0
= Update
那么在 commit 阶段,如何使用这个 flags 呢?流程如下:
commitMutationEffects(App)
App.subtreeFlags 有 Update → 子树里有活,往下找
│
├── commitMutationEffects(div)
│ div.subtreeFlags 有 Update → 继续往下
│ │
│ ├── commitMutationEffects(span)
│ │ span.flags 有 Update → 改 DOM!
│ │ span.subtreeFlags 是 0 → 子树没活,不用往下了
│ │
│ └── commitMutationEffects(b)
│ b.flags 是 0 → 自己没活
│ b.subtreeFlags 是 0 → 子树没活
│ 直接跳过 ✓
│
└── commitMutationEffects(Footer)
Footer.flags 是 0 → 没活
Footer.subtreeFlags 是 0 → 子树也没活
直接跳过 ✓ ← 整棵 Footer 子树一步跳过
如果没有冒泡的话,则会浪费大量的时间去做无用遍历:
没有冒泡 → commit 阶段必须遍历每一个 fiber 节点检查 flags
App → div → span → text → b → text → Footer → ...
大部分节点 flags 是 0,全是无用遍历
有冒泡 → 看一眼 subtreeFlags = 0 就跳过整棵子树
Footer 及其所有子孙 → 一步跳过
2.5. commit 阶段
commit 阶段分三个子阶段,按顺序同步执行,不可中断:
═══════ Commit 阶段 (同步不可中断) ═══════
│
│ commitRoot(root)
│ │
│ ├─── Before Mutation 阶段
│ │ ├─ ClassComponent → getSnapshotBeforeUpdate()
│ │ └─ 其他 → ...
│ │
│ ├─── Mutation 阶段 (操作真实 DOM)
│ │ 对每个 fiber(一遍遍历,利用 subtreeFlags 跳过无关子树):
│ │ │
│ │ ├─ ChildDeletion → 处理 fiber.deletions
│ │ │ ├─ FunctionComponent:
│ │ │ │ useInsertionEffect destroy()
│ │ │ │ useLayoutEffect destroy()
│ │ │ │ useEffect → 收集,等 flushPassiveEffects
│ │ │ ├─ ClassComponent → componentWillUnmount()
│ │ │ └─ HostComponent → removeChild
│ │ │
│ │ ├─ Placement → insertBefore / appendChild
│ │ │
│ │ └─ Update
│ │ ├─ FunctionComponent:
│ │ │ useInsertionEffect destroy → create
│ │ │ useLayoutEffect destroy(create 等 layout 阶段)
│ │ ├─ HostComponent → 应用 updateQueue 更新属性
│ │ └─ HostText → textNode.nodeValue = newText
│ │
│ ├─── 切换 current 指针: root.current = finishedWork
│ │
│ └─── Layout 阶段
│ ├─ ClassComponent → componentDidMount / componentDidUpdate
│ ├─ FunctionComponent → useLayoutEffect create(同步)
│ └─ Ref 赋值
│
▼
浏览器绘制 (Paint)
│
▼
flushPassiveEffects(异步调度,回调同步执行)
├─ 第一轮 destroy: 所有需要执行的 useEffect destroy()
│ + 被删组件的 useEffect destroy()
└─ 第二轮 create: 所有需要执行的 useEffect create()
2.5.1. beforeMutation(DOM 操作前)
commitBeforeMutationEffects
│
├─ 遍历 fiber 树(利用 subtreeFlags 跳过无关子树)
│
├─ ClassComponent(Snapshot flag):
│ 调用 getSnapshotBeforeUpdate(prevProps, prevState)
│ 在 DOM 被修改之前拍一张快照,保存一些即将被覆盖的信息。
│
├─ FunctionComponent:
│ 无事可做
│
└─ HostRoot:
清除容器内容(首次渲染时)
2.5.2. mutation (操作 DOM)
Tag 名称 含义
──────────────────────────────────────
HookHasEffect 这个 effect 的 deps 变了,需要执行
HookPassive 这是 useEffect
HookLayout 这是 useLayoutEffect
HookInsertion 这是 useInsertionEffect
commitMutationEffects 遍历 fiber 树:
对每个 fiber:
│
├── ① 检查 ChildDeletion flag → 处理 fiber.deletions 数组
│ forEach deletedFiber:
│ ├─ FunctionComponent:
│ │ ├── effect.tag & HookInsertion → destroy() 同步
│ │ ├── effect.tag & HookLayout → destroy() 同步
│ │ └── effect.tag & HookPassive → 收集到待清理列表,等 flushPassiveEffects
│ ├─ ClassComponent:
│ │ componentWillUnmount()
│ └─ HostComponent:
│ dom.removeChild(...)
│
├── ② 检查 Placement flag → dom.insertBefore() / dom.appendChild()
│ ├─ HostComponent:
│ │ 直接插入 fiber.stateNode
│ └─ FunctionComponent:
│ 自身无 DOM,透明处理:
│ ├── 往上穿透找最近的 HostComponent/HostRoot 作为父 DOM
│ └── 往下穿透找所有 HostComponent 子节点逐个插入
│
├── ③ 检查 Update flag
│ ├─ FunctionComponent: 遍历 fiber.updateQueue(Effect 环形链表):
│ │ ├── effect.tag & HookInsertion & HookHasEffect
│ │ │ → destroy() → create() destroy + create 一起,在 DOM 操作之前
│ │ │
│ │ └── effect.tag & HookLayout & HookHasEffect
│ │ → destroy() 只清理 create 等 layout 阶段
│ │ useEffect 不在这里处理
│ ├─ HostComponent:
│ │ 从 fiber.updateQueue 取出扁平数组 ['key', value, ...]
│ │ 逐对应用到 DOM:
│ │ ├── className → dom.className = value
│ │ ├── style → Object.assign(dom.style, value)
│ │ ├── children → dom.textContent = value(纯文本子节点)
│ │ ├── on* → 更新事件监听器
│ │ ├── value=null → dom.removeAttribute(key)(属性被删)
│ │ └── 其他 → dom.setAttribute(key, value)
│ │
│ ├─ HostText:
│ │ textNode.nodeValue = newText
│ └─ HostText:
│ 改 nodeValue
│
└── ④ 检查 Ref flag → 旧 ref 置 null
然后处理下一个 fiber...
2.5.3. 切换 current 树
root.current = finishedWork;
// 就这一行
// 从此刻起,wip 树变成了 current 树
// 下次渲染时它就是"旧的"
这一步在 mutation 之后、layout 之前,所以:
mutation 阶段读 current → 旧树(对的,因为在改 DOM 前/中需要旧信息)
layout 阶段读 current → 新树(对的,componentDidUpdate 看到的是新状态)
2.5.4. layout (DOM 已更新,浏览器还没绘制)
commitLayoutEffects(finishedWork, root)
│
遍历 fiber 树:
│
├─ FunctionComponent:
│ 遍历 fiber.updateQueue 的 Effect 链表
│ // 采用位操作:与
│ if (effect.tag & HookLayout && effect.tag & HookHasEffect) {
│ effect.destroy = effect.create() ← useLayoutEffect 的 create
│ }
│ │
│ └─ 此时 DOM 已更新,可以安全读取 DOM 尺寸/位置
│ 如果在 create 里 setState → 同步触发新一轮渲染
│ 用户不会看到中间状态
│
├─ ClassComponent:
│ ├─ mount → componentDidMount()
│ └─ update → componentDidUpdate(prevProps, prevState, snapshot)
│
├─ HostComponent:
│ ├─ 如果有 autoFocus 等 → dom.focus()
│ └─ ...
│
└─ 处理 Ref:
if (flags & Ref) {
if (typeof ref === 'function') ref(fiber.stateNode)
else ref.current = fiber.stateNode ← ref 指向真实 DOM
}
2.5.5. 调度 useEffect - 浏览器绘制后
commitRoot 末尾:
if (rootDoesHavePassiveEffects) {
scheduleCallback(NormalPriority, flushPassiveEffects)
// 注册异步回调,不在这里执行
}
────── 浏览器绘制 ──────
flushPassiveEffects()
│
├─ 第一轮:执行 destroy(全部先执行完)
│ │
│ 遍历有 Passive flag 的 fiber:
│ 遍历 fiber.updateQueue:
│ if (effect.tag & HookPassive && effect.tag & HookHasEffect) {
│ effect.destroy?.() ← useEffect 上一轮返回的清理函数
│ }
│
└─ 第二轮:执行 create(destroy 全部完成后才开始)
│
遍历有 Passive flag 的 fiber:
遍历 fiber.updateQueue:
if (effect.tag & HookPassive && effect.tag & HookHasEffect) {
effect.destroy = effect.create() ← useEffect 的回调
// 如果里面有 setState → 触发新一轮更新
}
3. diff 算法
经典树的 diff 算法,时间复杂度为 O(n*3),react 基于三个假设,将时间复杂度降低为 O(n)
- 不同类型的元素产生不同的树:比如 div -> span,直接销毁不再深入比较子树
- 同层级比较,不跨层移动:只比较同一父节点下的子节点列表
- key 标识节点身份,开发者通过 key 告诉 react 哪一些节点可能被复用
3.1. flags 标记含义与 Commit 阶段的对应
Placement (0b0000000000000010) → commitPlacement() → DOM insertBefore/appendChild
Update (0b0000000000000100) → commitUpdate() → DOM 属性更新
Deletion (0b0000000000001000) → commitDeletion() → DOM removeChild + cleanup
ChildDeletion (0b0000000000010000) → 标记在父节点上,表示有子节点需删除
3.2. diff 的入口与分发
reconcileChildFibers 根据 newChild 的类型进行分发:
reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes)
│
├─ newChild 是 object 且非 null?
│ ├─ $$typeof === REACT_ELEMENT_TYPE
│ │ └─▶ reconcileSingleElement() ← 单个元素
│ ├─ $$typeof === REACT_PORTAL_TYPE
│ │ └─▶ reconcileSinglePortal()
│ └─ isArray(newChild)
│ └─▶ reconcileChildrenArray() ← 多个元素(数组)
│
├─ newChild 是 string 或 number?
│ └─▶ reconcileSingleTextNode() ← 文本节点
│
└─ 以上都不满足?
└─▶ deleteRemainingChildren() ← 清空所有旧子节点
3.3. 单节点 diff
当新的 children 只有一个 React 元素时触发,调用reconcileSingleElement,算法流程如下:
reconcileSingleElement(currentFirstChild (旧的第一个子 fiber), element (新的单个 ReactElement))
遍历旧的子 fiber 链表 (child → sibling → sibling → ...)
│
├─ 对每个 oldFiber:
│ │
│ ├─ key 相同?
│ │ ├─ YES → type 也相同?
│ │ │ ├─ YES → ★ 复用该 fiber ★
│ │ │ │ deleteRemainingChildren(剩余兄弟全部标记删除)
│ │ │ │ return useFiber(oldFiber, newProps)
│ │ │ │
│ │ │ └─ NO → key 对了但 type 变了
│ │ │ deleteRemainingChildren(从当前节点起全部删除)
│ │ │ break → 创建新 fiber
│ │ │
│ │ │ ↑ 为什么 key 相同 type 不同要全部删除?
│ │ │ 因为 key 唯一匹配,既然匹配到的 type 不同,
│ │ │ 后续兄弟也不可能匹配,全删最优。
│ │ │
│ └─ └─ NO → key 不同
│ deleteChild(仅删除当前 oldFiber)
│ continue → 检查下一个兄弟
│
│ ↑ 为什么 key 不同只删当前?
│ 因为目标 key 可能在后续兄弟中找到。
│
│
└─ 遍历完没找到可复用的
└─ createFiberFromElement(element) 创建全新 fiber
具体例子:
示例 1:key 相同,type 相同 → 复用
旧: <div key="a">old</div>
新: <div key="a">new</div>
key="a" === "a" ✓ type="div" === "div" ✓
→ 复用 fiber,仅更新 props (children: "old" → "new")
→ 标记 Update flag
示例 2:key 相同,type 不同 → 全部删除重建
旧: <div key="a"/>, <p key="b"/>, <span key="c"/>
新: <section key="a"/>
遍历旧节点:
oldFiber[0]: key="a" === "a" ✓, type="div" !== "section" ✗
→ 从此处起全部删除 (div, p, span 全标记 Deletion)
→ 创建新的 <section key="a"/>
示例 3:key 不同 → 逐个删除,继续找
旧: <div key="a"/>, <div key="b"/>, <div key="c"/>
新: <div key="b"/>
遍历旧节点:
oldFiber[0]: key="a" !== "b" → 删除 <div key="a"/>,继续
oldFiber[1]: key="b" === "b" ✓, type="div" === "div" ✓
→ 复用!删除剩余兄弟 <div key="c"/>
3.4. 多节点 diff
当新的 children 是数组时触发,调用 reconcileChildrenArray,这是业务中常见的 diff。React 团队发现实际场景中,节点更新的频率远高于增删和移动,因此算法优先处理更新的情况。
两轮遍历
reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes)
│
├─ 第一轮遍历:处理「更新」(最常见情况)
│ 从左到右逐个比较key和type,遇到不可复用的就停止
│
├─ 快速路径判断
│ ├─ 新数组遍历完?→ 删除剩余旧节点
│ └─ 旧链表遍历完?→ 剩余新节点全部创建
│
└─ 第二轮遍历:处理「移动/增删」
构建 Map<key, Fiber>,在剩余旧节点中查找可复用的
用 lastPlacedIndex 判断是否需要移动
Map 中剩余未匹配的 → 全部标记 Deletion
第一轮遍历(处理更新)
let oldFiber = currentFirstChild; // 旧链表头
let newIdx = 0; // 新数组下标
let lastPlacedIndex = 0; // 最后一个不需要移动的旧节点 index
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 旧 fiber 的 index 大于当前新下标,说明旧链表中有间隙
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; // 不前进旧指针
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 尝试复用: 比较 key 和 type
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
// key 不同,不可复用 → 跳出第一轮
break;
}
// 如果是新创建的 (oldFiber 存在但 type 不同),标记删除旧的
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
// 放置节点,更新 lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
oldFiber = nextOldFiber;
}
updateSlot 的判定逻辑:
updateSlot 返回 null 意味着 key 不同,第一轮立刻终止。
updateSlot(returnFiber, oldFiber, newChild):
│
├─ newChild 是文本/数字
│ ├─ oldFiber.key !== null → return null (key 不匹配)
│ └─ oldFiber.key === null → 尝试复用文本节点
│
└─ newChild 是 ReactElement
├─ newChild.key !== oldFiber.key → return null (key 不匹配,跳出循环)
└─ newChild.key === oldFiber.key
├─ type 相同 → useFiber() 复用
└─ type 不同 → createFiber() 新建
第一轮,具体例子:
情况 A:newIdx === newChildren.length (新数组遍历完了)
┌──────────────────────────────────────────┐
│ 旧: A → B → C → D │
│ 新: [A', B'] │
│ │
│ 第一轮: A 复用, B 复用, 新数组遍历完 │
│ → 删除剩余旧节点 C, D │
│ → 结束 │
└──────────────────────────────────────────┘
情况 B:oldFiber === null (旧链表遍历完了)
┌──────────────────────────────────────────┐
│ 旧: A → B │
│ 新: [A', B', C, D] │
│ │
│ 第一轮: A 复用, B 复用, 旧链表遍历完 │
│ → 为 C, D 创建新 fiber │
│ → 标记 Placement │
│ → 结束 │
└──────────────────────────────────────────┘
第二轮遍历(处理移动)
当第一轮中途跳出(key 不匹配),且新旧都有剩余节点时进入。
Step 1:构建旧节点 Map
// 将剩余旧节点放入 Map<key|index, fiber>
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Map 结构:
// key="c" → FiberC
// key="d" → FiberD
// key="e" → FiberE
// (没有 key 的用 index 作为 key)
Step 2:遍历剩余新节点,从 Map 中查找复用
for (; newIdx < newChildren.length; newIdx++) {
// 从 Map 中查找 key/index 匹配的旧 fiber
const newFiber = updateFromMap(
existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes
);
if (newFiber !== null) {
if (newFiber.alternate !== null) {
// 复用成功,从 Map 中移除(防止重复匹配)
existingChildren.delete(newFiber.key ?? newIdx);
}
// ★ 核心:判断是否需要移动 ★
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
}
// Map 中剩余的节点 → 全部标记删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
核心中的核心:placeChild 与 lastPlacedIndex
这是判断节点是否需要移动的算法,理解它就理解了整个多节点 Diff 的精髓。核心在于维护一个 lastPlacedIndex(上一个确认不需要移动的旧节点的 index)。如果当前复用节点在旧序列中的位置比 lastPlacedIndex 靠左,说明它需要移动到右边。
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
// 复用的节点:检查是否需要移动
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// ★ 旧位置在「最后不动锚点」左边 → 需要移动到右边
newFiber.flags |= Placement;
return lastPlacedIndex; // 锚点不变
} else {
// 旧位置在锚点右边或等于 → 不需要移动
return oldIndex; // 更新锚点为当前旧位置
}
} else {
// 全新节点:标记插入
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
第二轮,具体例子:
旧: A(0) → B(1) → C(2) → D(3) (括号内是 oldIndex)
新: [A, C, D, B]
═══ 第一轮遍历 ═══
i=0: new=A, old=A → key 相同, type 相同 → 复用
placeChild: oldIndex=0, lastPlacedIndex=0
0 >= 0 → 不移动, lastPlacedIndex=0
i=1: new=C, old=B → key 不同 → break!
═══ 第二轮遍历 ═══
构建 Map: { "B"→Fiber(1), "C"→Fiber(2), "D"→Fiber(3) }
i=1: new=C → Map 找到 Fiber(2)
placeChild: oldIndex=2, lastPlacedIndex=0
2 >= 0 → 不移动, lastPlacedIndex=2 ✅
i=2: new=D → Map 找到 Fiber(3)
placeChild: oldIndex=3, lastPlacedIndex=2
3 >= 2 → 不移动, lastPlacedIndex=3 ✅
i=3: new=B → Map 找到 Fiber(1)
placeChild: oldIndex=1, lastPlacedIndex=3
1 < 3 → ★ 需要移动! ★ 标记 Placement
lastPlacedIndex 保持 3
结果: A 不动, C 不动, D 不动, B 移动到末尾
DOM操作: 仅移动 B → 最少操作 ✓
旧: A(0) → B(1) → C(2) → D(3)
新: [D, A, B, C]
═══ 第一轮遍历 ═══
i=0: new=D, old=A → key 不同 → break!
═══ 第二轮遍历 ═══
构建 Map: { "A"→Fiber(0), "B"→Fiber(1), "C"→Fiber(2), "D"→Fiber(3) }
i=0: new=D → Map 找到 Fiber(3)
placeChild: oldIndex=3, lastPlacedIndex=0
3 >= 0 → 不移动, lastPlacedIndex=3
i=1: new=A → Map 找到 Fiber(0)
placeChild: oldIndex=0, lastPlacedIndex=3
0 < 3 → ★ 移动! ★
i=2: new=B → Map 找到 Fiber(1)
placeChild: oldIndex=1, lastPlacedIndex=3
1 < 3 → ★ 移动! ★
i=3: new=C → Map 找到 Fiber(2)
placeChild: oldIndex=2, lastPlacedIndex=3
2 < 3 → ★ 移动! ★
结果: D 不动, A/B/C 都标记移动
DOM操作: 3 次移动 (最坏情况)
实际上最优解只需移动 D 到头部 (1 次)
→ 这就是 React Diff 的局限性: 右移友好,左移代价高
旧: A(0) → B(1) → C(2) → D(3) → E(4)
新: [A, C, F, B]
═══ 第一轮遍历 ═══
i=0: new=A, old=A → 复用, lastPlacedIndex=0
i=1: new=C, old=B → key 不同 → break!
═══ 第二轮遍历 ═══
Map: { "B"→Fiber(1), "C"→Fiber(2), "D"→Fiber(3), "E"→Fiber(4) }
i=1: new=C → Map 找到 Fiber(2)
oldIndex=2 >= lastPlacedIndex=0 → 不动, lastPlacedIndex=2
i=2: new=F → Map 中没有 key="F"
→ 新建 fiber, 标记 Placement
i=3: new=B → Map 找到 Fiber(1)
oldIndex=1 < lastPlacedIndex=2 → ★ 移动!
Map 剩余: { "D"→Fiber(3), "E"→Fiber(4) }
→ D, E 标记 Deletion
最终 DOM 操作:
- A: 不动 (Update props if needed)
- C: 不动
- F: 插入 (Placement)
- B: 移动到末尾 (Placement)
- D: 删除 (Deletion)
- E: 删除 (Deletion)
4. hooks 实现
4.1. useState
基于上述的工作原理,那么我们可以试着实现一个 useState 的最小实现 demo(ps:只是一个小 demo,只实现核心的流程,后续 react版的完整实现,会在下一篇文章产出)
// action 可以是新值,也可以是 updater 函数
// 例如:setCount(1) 或 setCount(prev => prev + 1)
// Hook 节点:挂在 Fiber.memoizedState 链表上
function createHook() {
return {
memoizedState: null, // 当前 hook 对应的状态
queue: null, // 指向最后一个 update,形成环状链表
next: null, // 下一个 hook
}
}
// Fiber:这里只保留最小字段
function createFiber() {
return {
memoizedState: null, // hook 链表头
alternate: null, // 指向上一次渲染的 Fiber
}
}
// 当前正在渲染的 Fiber
let currentlyRenderingFiber = null
// 新 Fiber 上,当前正在构建的 hook
let workInProgressHook = null
// 旧 Fiber 上,当前正在读取的 hook
let currentHook = null
// 根 Fiber
let rootFiber = null
// 根组件和 props,方便 setState 后重新渲染
let rootComponent = null
let rootProps = null
// 是否正在渲染中
let isRendering = false
// 渲染阶段里如果触发了 setState,就在本轮结束后补跑一次
let didScheduleRenderPhaseUpdate = false
function resolveStateAction(prevState, action) {
return typeof action === "function" ? action(prevState) : action
}
function resolveInitialState(initialState) {
return typeof initialState === "function" ? initialState() : initialState
}
// 创建一个 update 节点
function createUpdate(action) {
return {
action,
next: null,
}
}
// 把 update 放进环状链表
function enqueueUpdate(queue, update) {
// queue.pending 指向最后一个 update
if (queue.pending === null) {
// 第一个 update,自环
update.next = update
} else {
// 插入到尾部后面
update.next = queue.pending.next
queue.pending.next = update
}
// pending 永远指向最后一个
queue.pending = update
}
// 消费 queue,依次算出最新 state
function processUpdateQueue(baseState, queue) {
const pending = queue.pending
if (pending === null) {
return baseState
}
let newState = baseState
const first = pending.next
let update = first
do {
newState = resolveStateAction(newState, update.action)
update = update.next
} while (update !== first)
// 本轮消费完,清空队列
queue.pending = null
return newState
}
// 开始渲染 hooks
function prepareToUseHooks(fiber) {
currentlyRenderingFiber = fiber
workInProgressHook = null
currentHook = fiber.alternate ? fiber.alternate.memoizedState : null
// 每次渲染都会重新构建当前 Fiber 的 hook 链表
fiber.memoizedState = null
}
// 结束渲染 hooks
function finishHooks() {
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// 把 hook 挂到当前 Fiber 的链表尾部
function pushHook(hook) {
if (currentlyRenderingFiber === null) {
throw new Error("Hooks can only be called inside a function component.")
}
if (workInProgressHook === null) {
// 第一个 hook,挂到 fiber.memoizedState
currentlyRenderingFiber.memoizedState = hook
} else {
// 后续 hook 挂到前一个 hook 的 next
workInProgressHook.next = hook
}
workInProgressHook = hook
return hook
}
// 调度重新渲染
function scheduleRender() {
if (rootComponent === null) {
return
}
// 如果正在 render 中,不要重入 render
// 只做标记,等当前 render 结束后再补跑
if (isRendering) {
didScheduleRenderPhaseUpdate = true
return
}
render(rootComponent, rootProps)
}
function useState(initialState) {
if (currentlyRenderingFiber === null) {
throw new Error("useState must be called during render.")
}
let hook
// update 阶段:从旧 hook 复制并计算新状态
if (currentHook !== null) {
const oldHook = currentHook
const queue = oldHook.queue
const newState = processUpdateQueue(oldHook.memoizedState, queue)
hook = createHook()
hook.memoizedState = newState
hook.queue = queue
pushHook(hook)
// currentHook 往后挪,供下一个 useState 使用
currentHook = oldHook.next
} else {
// mount 阶段:创建新 hook
hook = createHook()
hook.memoizedState = resolveInitialState(initialState)
hook.queue = { pending: null }
pushHook(hook)
}
// dispatch 永远往这个 hook 的 queue 里加更新
function dispatch(action) {
const update = createUpdate(action)
enqueueUpdate(hook.queue, update)
scheduleRender()
}
return [hook.memoizedState, dispatch]
}
function render(component, props) {
rootComponent = component
rootProps = props || {}
let result
do {
didScheduleRenderPhaseUpdate = false
isRendering = true
// 新建本轮的 workInProgress Fiber
const workInProgress = createFiber()
// alternate 指向旧 Fiber
workInProgress.alternate = rootFiber
prepareToUseHooks(workInProgress)
try {
// 执行函数组件,期间会调用 useState
result = component(rootProps)
} finally {
finishHooks()
isRendering = false
}
// 本轮渲染完成,把新 Fiber 变成当前根
rootFiber = workInProgress
// 如果渲染期间有人 setState,则继续补跑一轮
} while (didScheduleRenderPhaseUpdate)
return result
}
再给出一个运行例子
/**
* demo 代码
*/
function Counter() {
// 只保留一个 state,方便观察更新流程
const [count, setCount] = useState(0);
// render 阶段只读取 state
console.log("render ->", count);
// 返回事件处理函数,模拟 React 里的 onClick
return {
increment() {
setCount((prev) => prev + 1);
},
};
}
// 首次渲染
const app = render(Counter);
// 模拟用户点击按钮
app.increment(); // 0 -> 1
app.increment(); // 1 -> 2
用户点击 increment
|
v
dispatch(action)
|
|-- createUpdate(action)
|-- enqueueUpdate(queue, update)
|
v
scheduleRender()
|
v
render(Counter)
|
|================ 当前这一轮的新 Fiber ================
|
| new Fiber
| |
| |-- alternate ----------------------.
| \
| \
|================ 上一轮的旧 Fiber ========== \ =========
| \
| old Fiber \
| | \
| |-- Hook(state: 0, queue: update...) <------'
|
v
执行 Counter()
|
|-- useState(0)
|
|-- 读取 old Hook
|-- processUpdateQueue(baseState, queue)
|-- 算出 newState
|-- 创建 new Hook(state: 1)
|-- 挂到 new Fiber.memoizedState
|
v
render -> 1
|
v
rootFiber = new Fiber
4.2. useEffect
那么我们也可以试着实现一个 useEffect 的最小实现 demo(ps:同样也只是一个小 demo,后续 react版完整实现,同样也会在下一篇文章产出)
// Hook 节点:挂在 Fiber.memoizedState 链表上
function createHook() {
return {
memoizedState: null, // useState: state;useEffect: { deps, cleanup }
queue: null, // useState 用的更新队列
next: null, // 下一个 hook
};
}
// Fiber:这里只保留最小字段
function createFiber() {
return {
memoizedState: null, // hook 链表头
alternate: null, // 指向上一次渲染的 Fiber
effects: [], // 本轮 render 收集到的 effect
};
}
// 当前正在渲染的 Fiber
let currentlyRenderingFiber = null;
// 新 Fiber 上,当前正在构建的 hook
let workInProgressHook = null;
// 旧 Fiber 上,当前正在读取的 hook
let currentHook = null;
// 根 Fiber
let rootFiber = null;
// 根组件和 props,方便 setState 后重新渲染
let rootComponent = null;
let rootProps = null;
// 是否正在渲染中
let isRendering = false;
// 渲染阶段里如果触发了 setState,就在本轮结束后补跑一次
let didScheduleRenderPhaseUpdate = false;
function resolveStateAction(prevState, action) {
return typeof action === "function" ? action(prevState) : action;
}
function resolveInitialState(initialState) {
return typeof initialState === "function" ? initialState() : initialState;
}
// 创建一个 update 节点
function createUpdate(action) {
return {
action,
next: null,
};
}
// 把 update 放进环状链表
function enqueueUpdate(queue, update) {
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
}
// 消费 queue,依次算出最新 state
function processUpdateQueue(baseState, queue) {
const pending = queue.pending;
if (pending === null) {
return baseState;
}
let newState = baseState;
const first = pending.next;
let update = first;
do {
newState = resolveStateAction(newState, update.action);
update = update.next;
} while (update !== first);
queue.pending = null;
return newState;
}
// 开始渲染 hooks
function prepareToUseHooks(fiber) {
currentlyRenderingFiber = fiber;
workInProgressHook = null;
currentHook = fiber.alternate ? fiber.alternate.memoizedState : null;
fiber.memoizedState = null;
fiber.effects = [];
}
// 结束渲染 hooks
function finishHooks() {
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
}
// 把 hook 挂到当前 Fiber 的链表尾部
function pushHook(hook) {
if (currentlyRenderingFiber === null) {
throw new Error("Hooks can only be called inside a function component.");
}
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
return hook;
}
// 调度重新渲染
function scheduleRender() {
if (rootComponent === null) {
return;
}
if (isRendering) {
didScheduleRenderPhaseUpdate = true;
return;
}
render(rootComponent, rootProps);
}
// 极简版 useState
function useState(initialState) {
if (currentlyRenderingFiber === null) {
throw new Error("useState must be called during render.");
}
let hook;
if (currentHook !== null) {
const oldHook = currentHook;
const queue = oldHook.queue;
const newState = processUpdateQueue(oldHook.memoizedState, queue);
hook = createHook();
hook.memoizedState = newState;
hook.queue = queue;
pushHook(hook);
currentHook = oldHook.next;
} else {
hook = createHook();
hook.memoizedState = resolveInitialState(initialState);
hook.queue = { pending: null };
pushHook(hook);
}
function dispatch(action) {
const update = createUpdate(action);
enqueueUpdate(hook.queue, update);
scheduleRender();
}
return [hook.memoizedState, dispatch];
}
// 比较两次依赖数组是否完全相等
// React 里也是逐项用 Object.is 比较
function areHookInputsEqual(nextDeps, prevDeps) {
// 只要有一边是 null,就认为不能复用
// 这里的 null 表示“没有传 deps”,这种情况应该每次都执行
if (prevDeps === null || nextDeps === null) {
return false;
}
// 长度不同,直接认为依赖变化
if (nextDeps.length !== prevDeps.length) {
return false;
}
// 逐项比较
for (let i = 0; i < nextDeps.length; i += 1) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false;
}
}
return true;
}
// 统一执行本轮 render 收集到的 effects
function flushEffects(effects) {
for (const effect of effects) {
// 如果上一次 effect 返回过 cleanup,
// 并且这次依赖发生变化了,
// 那么先执行旧 cleanup
if (typeof effect.prevCleanup === "function") {
effect.prevCleanup();
}
// 再执行新的 effect 回调
// effect 回调可以选择返回 cleanup 函数
const cleanup = effect.create();
// 把新的 cleanup 保存回当前 hook 上
// 供下次依赖变化时执行
effect.hook.memoizedState.cleanup =
typeof cleanup === "function" ? cleanup : null;
}
}
// 极简版 useEffect
function useEffect(create, deps) {
// 和 useState 一样,只能在函数组件 render 阶段调用
if (currentlyRenderingFiber === null) {
throw new Error("useEffect must be called during render.");
}
// 不传 deps 时,表示每次 render 后都执行
// 这里统一用 null 表示“无依赖数组”
const nextDeps = deps === undefined ? null : deps;
let hook;
// update 阶段:当前有旧 hook 可以复用
if (currentHook !== null) {
const oldHook = currentHook;
// oldHook.memoizedState 里保存的是上一次 effect 的信息
// 结构是:{ deps, cleanup }
const prevEffect = oldHook.memoizedState;
// 为本轮创建一个新的 hook 节点
hook = createHook();
// 先把本轮 deps 记下来
// cleanup 先沿用旧的,等 flushEffects 真正执行完新的 effect 后再覆盖
hook.memoizedState = {
deps: nextDeps,
cleanup: prevEffect.cleanup,
};
// 挂到当前 Fiber 的 hook 链表上
pushHook(hook);
// 旧 hook 指针后移,供下一个 hook 使用
currentHook = oldHook.next;
// 判断这次 effect 是否需要执行
const shouldRun =
// 没传 deps:每次都执行
nextDeps === null ||
// 上一次没传 deps:也认为需要执行
prevEffect.deps === null ||
// 依赖数组不同:需要执行
!areHookInputsEqual(nextDeps, prevEffect.deps);
// 只有依赖变化了,才把 effect 放入本轮待执行列表
if (shouldRun) {
currentlyRenderingFiber.effects.push({
create, // 新的 effect 回调
hook, // 当前新 hook,执行后要把 cleanup 写回这里
prevCleanup: prevEffect.cleanup, // 上一次的 cleanup,执行新 effect 前先清理
});
}
} else {
// mount 阶段:首次渲染,没有旧 hook
hook = createHook();
// 首次挂载时先记录 deps,cleanup 还没有
hook.memoizedState = {
deps: nextDeps,
cleanup: null,
};
// 挂到当前 Fiber 的 hook 链表
pushHook(hook);
// 首次挂载一定要执行一次 effect
currentlyRenderingFiber.effects.push({
create,
hook,
prevCleanup: null,
});
}
}
function render(component, props) {
rootComponent = component;
rootProps = props || {};
let result;
do {
didScheduleRenderPhaseUpdate = false;
isRendering = true;
// 创建本轮 workInProgress Fiber
const workInProgress = createFiber();
// alternate 指向上一次渲染结果
workInProgress.alternate = rootFiber;
// 准备开始读取 / 构建 hooks
prepareToUseHooks(workInProgress);
try {
// 执行组件函数
// 期间 useState / useEffect 会依次挂到 Fiber.memoizedState 链表上
result = component(rootProps);
} finally {
finishHooks();
isRendering = false;
}
// 本轮 render 完成后,当前 Fiber 成为新的 rootFiber
rootFiber = workInProgress;
// 如果 render 过程中触发了 setState,就补跑下一轮 render
} while (didScheduleRenderPhaseUpdate);
// 重点:
// 只有 render 全部稳定结束后,才统一执行本轮收集到的 effect
const effects = rootFiber.effects.slice();
rootFiber.effects.length = 0;
flushEffects(effects);
return result;
}
再给一个运行例子
function Counter() {
// 只保留一个 state,方便观察 state 和 effect 的关系
const [count, setCount] = useState(0);
console.log("render ->", count);
// count 变化后,render 结束再执行 effect
useEffect(() => {
console.log("effect: count changed ->", count);
return () => {
console.log("cleanup: previous count ->", count);
};
}, [count]);
return {
increment() {
setCount((prev) => prev + 1);
},
};
}
const app = render(Counter);
app.increment();
app.increment();
render(Counter)
|
|================ 当前这一轮的新 Fiber ================
|
| new Fiber
| |
| |-- memoizedState
| | |
| | -> Hook1(state: count)
| | |
| | -> Hook2(effect: { deps, cleanup })
| |
| -> effects[]
|
v
执行 Counter()
|
|-- useState(0)
| |
| -> 读/写 state hook
|
|-- useEffect(create, [count])
|
|-- 读取 old Hook2 的 deps/cleanup
|-- 比较 old deps 和 new deps
|
|-- 如果 deps 变了:
| 把 effect 放进 new Fiber.effects
|
|-- 注意:这里还不执行 create
|
v
render 结束
|
v
flushEffects(new Fiber.effects)
|
|-- 先执行 prevCleanup()
|-- 再执行 create()
|-- 拿到新的 cleanup
|-- 保存到 Hook2.memoizedState.cleanup
|
v
下一轮更新继续复用