`)
- **`flags` 更新**:
DivFiber 自身的 props/结构未变化(仅子树 SpanFiber 及其后代变化),因此 `flags` 保持 `NoFlags`。
→ `DivFiber.flags = NoFlags (0)`。
- **`subTreeFlags` 更新**:
在 `completeWork` 阶段,DivFiber 聚合子节点 SpanFiber 的 `flags` 和 `subTreeFlags`:
`DivFiber.subTreeFlags |= SpanFiber.flags | SpanFiber.subTreeFlags`
→ `0 | (0 | 1) = 1`(即 `Update`)。
→ `DivFiber.subTreeFlags = Update (1)`。
4. ChildFiber(曾祖父节点,标签为 `FunctionComponent`,对应 `Child` 组件)
- **`flags` 更新**:
Child 组件自身的逻辑未变化(仅渲染的子树 DivFiber 变化),因此 `flags` 保持 `NoFlags`。
→ `ChildFiber.flags = NoFlags (0)`。
- **`subTreeFlags` 更新**:
在 `completeWork` 阶段,ChildFiber 聚合子节点 DivFiber 的 `flags` 和 `subTreeFlags`:
`ChildFiber.subTreeFlags |= DivFiber.flags | DivFiber.subTreeFlags`
→ `0 | (0 | 1) = 1`(即 `Update`)。
→ `ChildFiber.subTreeFlags = Update (1)`。
5. ParentFiber(根节点,标签为 `FunctionComponent`,对应 `Parent` 组件)
- **`flags` 更新**:
Parent 组件自身未变化,`flags` 保持 `NoFlags`。
→ `ParentFiber.flags = NoFlags (0)`。
- **`subTreeFlags` 更新**:
在 `completeWork` 阶段,ParentFiber 聚合子节点 ChildFiber 的 `flags` 和 `subTreeFlags`:
`ParentFiber.subTreeFlags |= ChildFiber.flags | ChildFiber.subTreeFlags`
→ `0 | (0 | 1) = 1`(即 `Update`)。
→ `ParentFiber.subTreeFlags = Update (1)`。
最终状态总结
| Fiber 节点 | `flags`(自身更新) | `subTreeFlags`(子树更新) | 说明 |
|--------------|---------------------|---------------------------|--------------------------|
| TextFiber | `Update (1)` | `NoFlags (0)` | 自身内容更新 |
| SpanFiber | `NoFlags (0)` | `Update (1)` | 子树(TextFiber)有更新 |
| DivFiber | `NoFlags (0)` | `Update (1)` | 子树(SpanFiber)有更新 |
| ChildFiber | `NoFlags (0)` | `Update (1)` | 子树(DivFiber)有更新 |
| ParentFiber | `NoFlags (0)` | `Update (1)` | 子树(ChildFiber)有更新 |
另一种场景:同时更新 SpanFiber 和 TextFiber
如果不仅 TextFiber 内容更新(`Update`),SpanFiber 还需要添加 `className`(自身 `Update`),则:
- SpanFiber 的 `flags` 会被标记为 `Update (1)`;
- SpanFiber 的 `subTreeFlags` 聚合 TextFiber 的 `Update (1)`,结果为 `1 | 1 = 1`(仍为 `Update`);
- 上层节点(DivFiber、ChildFiber 等)的 `subTreeFlags` 依然会聚合为 `Update`(因为位运算 `|` 不会重复计算相同标记)。
核心结论
- `flags` 只关注**自身是否有更新**(如内容、props 变化),仅当前节点有操作时才会被标记。
- `subTreeFlags` 关注**子树是否有更新**(无论自身是否更新),通过“自底向上”的位运算聚合所有后代节点的 `flags`,让上层节点无需遍历子树即可快速判断是否有更新,从而优化性能
## 渲染核心流程
- 创建初始化 WIP
- 进入“递归”的“递”流程
主要是 beginWork 函数逻辑:
- 处理 Fiber 节点的父子、兄弟节点的关系,涉及:child、return、sibling
- 给 Fiber 节点打标记 flags、subTreeFlags
- 进入“递归”的“归”流程
- 处理真实 Dom 节点关系
- 解析 flags 标记、合并处理 subTreeFlags
- 处理父子 Dom 真实的节点关系,将子节点插入父节点中
- 进入 commit 流程,进行真实的 Dom 渲染
### 递归的“递”阶段做了哪些事情?
#### 渲染阶段(构建 workInProgress 树)
```javascript
// 开始渲染时,创建 workInProgress 树
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
// 从 current 树克隆出 workInProgress 树
root.workInProgress = createWorkInProgress(root.current, null);
}
// 协调过程在 workInProgress 树上进行
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// 在 workInProgress 树上执行协调
workLoopSync();
// 协调完成,workInProgress 树构建完毕
const finishedWork: FiberNode = root.current.alternate;
root.finishedWork = finishedWork;
}
```
### 递归的“归”阶段做了哪些事情?
#### 提交阶段(树切换)
```javascript
function commitRoot(root: FiberRoot) {
const finishedWork = root.finishedWork;
if (finishedWork !== null) {
// === 准备提交 ===
root.finishedWork = null;
// === 执行 DOM 操作 ===
commitMutationEffects(root, finishedWork);
// === 关键:切换 current 指针 ===
root.current = finishedWork; // 🎯 双缓冲切换!
// === 执行 layout 效果 ===
commitLayoutEffects(root, finishedWork);
}
}
```
#### 变更前 commitBeforeMutationEffects
React 的 commitRoot 分为3个阶段
```javascript
function commitRoot(root: FiberRoot) {
// 完整版本,包含所有三个阶段
commitBeforeMutationEffects(root, finishedWork); // 阶段1
commitMutationEffects(root, finishedWork); // 阶段2
commitLayoutEffects(root, finishedWork); // 阶段3
// 注意:树切换在内部处理
}
```
变更前执行 commitBeforeMutationEffects。主要负责在 DOM 发生变更(mutation)之前 执行一系列必要的准备工作,为后续的 DOM 操作(如插入、删除、更新节点)铺路。
```javascript
function commitBeforeMutationEffects(root: FiberRoot, finishedWork: Fiber) {
// 读取 DOM 状态快照(如滚动位置)
// 调用 getSnapshotBeforeUpdate 生命周期
// 不涉及 DOM 修改
}
```
核心作用:commitBeforeMutationEffects 的核心目标是:在真正修改 DOM 结构之前,处理一些依赖于「当前 DOM 状态」的逻辑,确保这些逻辑能获取到 DOM 变更前的状态,同时完成一些前置准备。
#### 变更 commitMutationEffects
```javascript
function commitMutationEffects(root: FiberRoot, finishedWork: Fiber) {
// 遍历并执行所有 DOM 操作:
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
}
```
DOM 操作:
```javascript
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
if (flags & Placement) {
// 🎯 插入 DOM 节点到父节点
commitPlacement(finishedWork);
}
if (flags & Update) {
// 🎯 更新 DOM 属性和内容
commitUpdate(finishedWork);
}
if (flags & Deletion) {
// 🎯 删除 DOM 节点
commitDeletion(finishedWork);
}
}
```
#### 布局阶段 commitLayoutEffects
```javascript
function commitLayoutEffects(root: FiberRoot, finishedWork: Fiber) {
// DOM 已经更新完成,执行:
// - componentDidMount / componentDidUpdate
// - useLayoutEffect 回调
// - refs 的赋值和清理
// - 调度 useEffect
}
```
commitLayoutEffects 是 DOM 变更(mutation)之后的关键步骤,核心作用是:在 DOM 已经完成更新后,处理所有依赖于「最新 DOM 布局状态」的逻辑(比如读取元素尺寸、位置,更新 refs,触发布局相关的生命周期等)
虽然浏览器还没自动触发 layout,但在 commitLayoutEffects 阶段,如果你主动读取 DOM 的布局信息(如 offsetHeight、getBoundingClientRect() 等),浏览器会被 “强制” 立即执行 layout 计算(因为需要返回最新的结果)。
这正是 commitLayoutEffects 的设计用意:
此时 DOM 已更新,读取布局信息能拿到最新结果;
强制 layout 后,你可以同步修改 DOM 样式(如调整位置、尺寸),这些修改会被浏览器合并到后续的绘制中,不会导致额外的 layout 开销(避免 “布局抖动”)
比如
```javascript
useLayoutEffect(() => {
// 1. 读取 offsetHeight:强制浏览器立即计算 layout(因为 DOM 已更新但未 layout)
const height = ref.current.offsetHeight;
// 2. 同步修改样式:此时修改会被浏览器合并,不会触发二次 layout
ref.current.style.marginTop = `${100 - height}px`;
}, []);
```
### 双缓冲切换的详细过程
React 采用了类似 iOS/Android 中图形渲染的双缓冲机制来实现无撕裂的 UI 更新
```javascript
// React 同时维护两棵 Fiber 树:
let current: FiberNode; // 当前显示在屏幕上的树(前缓冲区)
let workInProgress: FiberNode; // 正在构建的新树(后缓冲区)
```
#### 切换前的状态
```javascript
// 切换前:
FiberRoot {
current: FiberNode_A, // 屏幕上显示的是树A
finishedWork: FiberNode_B, // 刚构建完的树B
}
// 两棵树通过 alternate 互相引用:
FiberNode_A.alternate = FiberNode_B;
FiberNode_B.alternate = FiberNode_A;
```
#### 切换后的状态
```javascript
// 切换 current 指针。类似 iOS 双缓冲机制,视频控制器在收到 V-Sync 信号后,GPU 切换画面
root.current = root.finishedWork
// 切换后
FiberRoot {
current: FiberNode_B, // 现在屏幕上显示树B
finishedWork: null // 准备下一轮构建
}
```
### 双缓冲好处
#### 无撕裂更新
没有双缓冲的问题:用户可能看到部分更新的UI(比如旧的 header + 新的 content)
有双缓冲:所有 DOM 操作在后台完成,然后一次性切换到新树。用户看到的是完整的、一致的 UI
#### 可中断渲染
```javascript
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
// 如果被打断,下次可以从断点继续
// workInProgress 树保持中间状态,不影响 current 树
}
```
#### 状态一致性
- 在渲染过程中,current 树始终保持不变
- 用户可以继续与当前UI交互
#### 实际例子
```javascript
// 假设我们有这样的组件更新:
function App() {
const [count, setCount] = useState(0);
return (
Current: {count}
);
}
```
点击事件触发后的完整流程伪代码为:
```javascript
// 1. 开始渲染
const root = FiberRootNode;
root.current = Tree_A; // 当前显示Tree_A
// 2. 准备workInProgress树
root.workInProgress = createWorkInProgress(Tree_A, null);
// 现在有:Tree_A (current) 和 Tree_B (workInProgress)
// 3. 在Tree_B上执行协调
// - 更新button的文本
// - 更新span的文本
// - 计算DOM变更
// 4. 协调完成
root.finishedWork = Tree_B;
// 5. 提交阶段
commitRoot(root);
// - 执行DOM更新(修改文本内容)
// - 🎯 root.current = Tree_B; // 切换到新树
// - Tree_B现在成为current树
// 6. 准备下一轮更新
root.workInProgress = null;
// 下次更新时,会从Tree_B克隆出新的workInProgress树
```
## React 的合成事件
React 将事件统一绑定在根节点(React 17 之前是 document,React 17 及之后是 React 应用挂载的根 DOM 节点)上,主要是基于性能优化、兼容性处理和架构设计等方面的综合考虑。这个机制被称为 “合成事件(SyntheticEvent)” 和 “事件委托(Event Delegation)
### 深入了解事件委托机制
在 React 中,你通过 JSX 编写的事件处理函数(如 onClick、onChange)并不会直接绑定到对应的 DOM 元素上。React 实际做的事情是:
- 统一监听:在应用的根节点(React 17+ 是你挂载 React 应用的 DOM 节点,如 ReactDOM.createRoot(document.getElementById('root')) 中的 #root;之前是 document)上设置一个统一的事件监听器。
- 事件触发:当任何子元素发生事件(比如点击),由于事件冒泡机制,这个事件会最终冒泡到根节点。
- 映射处理:根节点上的统一监听器捕获事件后,React 会根据事件触发的源 DOM 元素和事件类型,在自己的映射关系中找到对应组件的事件处理函数并执行。
React 17 的变化提醒:需要注意的是,在 React 17 及之后的版本中,事件委托的节点从 document 变为了你渲染 React 树的根 DOM 容器。这个改动使得多个 React 版本共存时事件系统可以更好地隔离。
好处:
- 简化事件处理:开发者无需手动管理事件的绑定 (addEventListener) 和解绑 (removeEventListener),React 已在内部处理,降低了代码复杂度和内存泄漏风险。
- 功能增强:合成事件提供了与浏览器原生事件相同的接口,并在某些情况下进行了增强,确保在不同浏览器中有一致的行为
你在某个组件上写的事件代码,本质上都是添加到根 Dom 节点上的。
```javascript
// 你在组件中写的:
function Button() {
const handleClick = () => console.log('Clicked!');
return
;
}
// 🔧 React实际做的事情:
// 1. 只在根元素上绑定一个真实的事件监听器
rootElement.addEventListener('click', (nativeEvent) => {
// 2. 找到实际被点击的DOM元素
const targetElement = nativeEvent.target;
// 3. 根据DOM元素找到对应的React组件和事件处理函数
const syntheticEvent = createSyntheticEvent(nativeEvent);
dispatchEvent(targetElement, 'click', syntheticEvent);
});
```
## setState 是单向循环链表结构
跟随源码来看看执行流程
```javascript
// 递归中的递阶段
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
// 比较,返回子fiberNode
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip, renderLane);
// ...
};
```
会调用 updateHostRoot 方法
```javascript
function updateHostRoot(wip: FiberNode, renderLane: Lane) {
const baseState = wip.memoizedState;
const updateQueue = wip.updateQueue as UpdateQueue
;
const pending = updateQueue.shared.pending;
updateQueue.shared.pending = null;
const prevChildren = wip.memoizedState;
const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
wip.memoizedState = memoizedState;
const current = wip.alternate;
// 考虑RootDidNotComplete的情况,需要复用memoizedState
if (current !== null) {
if (!current.memoizedState) {
current.memoizedState = memoizedState;
}
}
const nextChildren = wip.memoizedState;
if (prevChildren === nextChildren) {
return bailoutOnAlreadyFinishedWork(wip, renderLane);
}
reconcileChildren(wip, nextChildren);
return wip.child;
}
```
接着会调用 processUpdateQueue 方法
```javascript
export const processUpdateQueue = (
baseState: State,
pendingUpdate: Update | null,
renderLane: Lane,
onSkipUpdate?: (update: Update) => void
): {
memoizedState: State;
baseState: State;
baseQueue: Update | null;
} => {
const result: ReturnType> = {
memoizedState: baseState,
baseState,
baseQueue: null
};
if (pendingUpdate !== null) {
// 第一个update
const first = pendingUpdate.next;
let pending = pendingUpdate.next as Update;
let newBaseState = baseState;
let newBaseQueueFirst: Update | null = null;
let newBaseQueueLast: Update | null = null;
let newState = baseState;
do {
const updateLane = pending.lane;
if (!isSubsetOfLanes(renderLane, updateLane)) {
// 跳过低优先级更新,但保留在链表中(优先级不够 被跳过)
const clone = createUpdate(pending.action, pending.lane);
onSkipUpdate?.(clone);
// 是不是第一个被跳过的
if (newBaseQueueFirst === null) {
// first u0 last = u0
newBaseQueueFirst = clone;
newBaseQueueLast = clone;
newBaseState = newState;
} else {
// first u0 -> u1 -> u2
// last u2
(newBaseQueueLast as Update).next = clone;
newBaseQueueLast = clone;
}
} else {
// 优先级足够
if (newBaseQueueLast !== null) {
const clone = createUpdate(pending.action, NoLane);
newBaseQueueLast.next = clone;
newBaseQueueLast = clone;
}
const action = pending.action;
if (pending.hasEagerState) {
newState = pending.eagerState;
} else {
newState = basicStateReducer(baseState, action);
}
}
pending = pending.next as Update;
} while (pending !== first);
if (newBaseQueueLast === null) {
// 本次计算没有update被跳过
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
result.memoizedState = newState;
result.baseState = newBaseState;
result.baseQueue = newBaseQueueLast;
}
return result;
};
```
`pending !== first` 设计为一个单向循环链表来方便遍历,为什么设计为双向循环链表?
前置知识:循环链表相比数组,在插入和删除方面更加高效。因为数组在插入和删除方面需要时间复杂度为 O(n)
React 的并发特性允许高优先级更新中断低优先级更新,使用循环链表数据结构可以满足:
- 保存中断点(记住当前节点)
- 从中断处继续执行
- 插入高优先级更新找到合适位置
使用其他的数据结构也能做,就是效率很低,循环链表更适合做这个事。
QA:什么是高优先级更新、什么是低优先级更新?
高优先级更新(同步/用户阻塞)
- 用户交互:点击、输入、滚动
- 动画:CSS transitions/animations
- 生命周期方法:componentDidMount等
低优先级更新(并发/可中断)
- 数据获取:API调用结果
- 懒加载:非关键内容加载
- 大计算量:复杂数据处理
## React Hooks 链表结构
在 React 中,**所有 Hooks(包括 useState、useEffect、useRef 等)都是通过一个单向链表结构来管理的**,并非只有 useState 如此。这个链表是 React 内部用于追踪组件中 Hooks 调用顺序和状态的核心机制。
### 为什么用链表管理 Hooks?
React Hooks 的设计要求“必须在组件顶层调用”(不能在条件、循环、嵌套函数中调用),这正是因为 Hooks 的状态依赖于**调用顺序**。链表结构天然适合按顺序记录和访问 Hooks 的信息(如状态值、更新函数、依赖项等),确保每次渲染时 Hooks 的顺序与首次渲染一致,从而正确匹配对应的状态。
### Hooks 链表的工作流程
React 会为每个组件实例维护一个独立的 Hooks 链表,其核心工作流程可分为**首次渲染**和**重新渲染**两个阶段:
#### 1. 首次渲染(Mount 阶段)
- 当组件首次渲染时,React 会初始化一个 `workInProgressHook` 指针(指向当前处理的 Hook 节点),并创建一个空的 Hooks 链表。
- 每调用一个 Hook(如 `useState`),React 会创建一个对应的 **Hook 节点**(包含该 Hook 的状态、更新函数、依赖项等信息),并将其添加到链表的末尾。
- 同时,`workInProgressHook` 指针会向后移动,指向新创建的节点,确保下一个 Hook 按顺序衔接。
例如,组件中调用两个 Hooks:
```jsx
function MyComponent() {
const [count, setCount] = useState(0); // 第一个 Hook 节点
useEffect(() => {}, []); // 第二个 Hook 节点
return {count}
;
}
```
首次渲染后,链表结构为:`useState 节点 -> useEffect 节点 -> null`。
#### 2. 重新渲染(Update 阶段)
- 当组件因状态更新(如调用 `setCount`)重新渲染时,React 会重置 `workInProgressHook` 指针到链表的头部。
- 再次按顺序调用 Hooks 时,React 会通过指针依次访问链表中已有的节点,直接复用或更新节点中的信息(而非重新创建节点)。
- 对于 `useState`:直接读取节点中保存的最新状态值,并返回更新函数。
- 对于 `useEffect`:检查当前依赖项与节点中保存的旧依赖项是否一致,决定是否执行副作用回调。
这种“按顺序复用节点”的机制,正是 Hooks 能在多次渲染中保持状态的核心原因。
### 关键注意点
- **顺序必须严格一致**:如果在条件语句中调用 Hook(如 `if (condition) { useState() }`),会导致重新渲染时 Hooks 调用顺序与首次渲染不一致,链表指针无法正确匹配节点,最终引发状态错乱或报错。
- **每个组件独立维护链表**:不同组件的 Hooks 链表是隔离的,互不影响(通过组件的 Fiber 节点关联各自的 Hooks 链表)。
总结:**所有 Hooks 共同组成一个单向链表**,React 通过维护这个链表并严格遵循调用顺序,实现了 Hooks 在组件多次渲染中的状态追踪和复用。这一机制是 Hooks 设计的底层基础。
## useEffect 工作原理
### 实验说明
AComponent
```javascript
import { useEffect } from "react"; // 注意:是 useEffect 而非 use
export default function AComponent(props) {
// 普通函数:仅在 useEffect 回调中被调用,不是独立副作用
const effect1 = () => {
console.log("Effect 1(A组件的副作用逻辑1)");
return () => console.log("clean effect 1(A组件的清理逻辑1)");
};
const effect2 = () => {
console.log("Effect 2(A组件的副作用逻辑2)");
return () => console.log("clean effect 2(A组件的清理逻辑2)");
};
// A组件的唯一 useEffect 调用(生成1个 Effect 对象)
useEffect(() => {
// 回调中依次执行普通函数(属于该 Effect 对象的逻辑)
effect1();
effect2();
}, []);
return (
AComponent(父组件)
{props.children} {/* 渲染子组件B */}
);
}
```
BComponent
```javascript
// BComponent.jsx
import { useEffect } from "react";
export default function BComponent() {
const effect3 = () => {
console.log("Effect 3(B组件的副作用逻辑1)");
return () => console.log("clean effect 3(B组件的清理逻辑1)");
};
const effect4 = () => {
console.log("Effect 4(B组件的副作用逻辑2)");
return () => console.log("clean effect 4(B组件的清理逻辑2)");
};
// B组件的唯一 useEffect 调用(生成1个 Effect 对象)
useEffect(() => {
effect3();
effect4();
}, []);
return BComponent(子组件)
;
}
```
App.js
```javascript
// App.jsx
import AComponent from "./AComponent";
import BComponent from "./BComponent";
function App() {
// 渲染结构:A是父组件,B是A的子组件
return (
);
}
export default App;
```
上面的代码会输出:
```javascript
Effect 3(B组件的副作用逻辑1)
Effect 4(B组件的副作用逻辑2)
Effect 1(A组件的副作用逻辑1)
Effect 2(A组件的副作用逻辑2)
```
为什么会这样输出??
### 源码探究
```javascript
// 递归中的递阶段
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
// bailout策略
didReceiveUpdate = false;
const current = wip.alternate;
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = wip.pendingProps;
// 四要素~ props type
// {num: 0, name: 'cpn2'}
// {num: 0, name: 'cpn2'}
if (oldProps !== newProps || current.type !== wip.type) {
didReceiveUpdate = true;
} else {
// state context
const hasScheduledStateOrContext = checkScheduledUpdateOrContext(
current,
renderLane
);
if (!hasScheduledStateOrContext) {
// 四要素~ state context
// 命中bailout
didReceiveUpdate = false;
switch (wip.tag) {
case ContextProvider:
const newValue = wip.memoizedProps.value;
const context = wip.type._context;
pushProvider(context, newValue);
break;
// TODO Suspense
}
return bailoutOnAlreadyFinishedWork(wip, renderLane);
}
}
}
wip.lanes = NoLanes;
// 比较,返回子fiberNode
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip, renderLane);
case HostComponent:
return updateHostComponent(wip);
case HostText:
return null;
case FunctionComponent:
return updateFunctionComponent(wip, wip.type, renderLane);
case Fragment:
return updateFragment(wip);
case ContextProvider:
return updateContextProvider(wip, renderLane);
case SuspenseComponent:
return updateSuspenseComponent(wip);
case OffscreenComponent:
return updateOffscreenComponent(wip);
case LazyComponent:
return mountLazyComponent(wip, renderLane);
case MemoComponent:
return updateMemoComponent(wip, renderLane);
default:
if (__DEV__) {
console.warn('beginWork未实现的类型');
}
break;
}
return null;
};
```
判断 fiber 节点的 tag 为 FunctionComponent 则执行 updateFunctionComponent 流程
```javascript
function updateFunctionComponent(
wip: FiberNode,
Component: FiberNode['type'],
renderLane: Lane
) {
prepareToReadContext(wip, renderLane);
// render
const nextChildren = renderWithHooks(wip, Component, renderLane);
const current = wip.alternate;
if (current !== null && !didReceiveUpdate) {
bailoutHook(wip, renderLane);
return bailoutOnAlreadyFinishedWork(wip, renderLane);
}
reconcileChildren(wip, nextChildren);
return wip.child;
}
```
可以看到在 updateFunctionComponent 内部执行了 renderWithHooks 函数
```javascript
export function renderWithHooks(
wip: FiberNode,
Component: FiberNode['type'],
lane: Lane
) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置 hooks链表
wip.memoizedState = null;
// 重置 effect链表
wip.updateQueue = null;
renderLane = lane;
const current = wip.alternate;
if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}
const props = wip.pendingProps;
// FC render
const children = Component(props);
// 重置操作
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
renderLane = NoLane;
return children;
}
```
### useState 循环链表
对 useState 下断点会走到 pushEffect 的逻辑里,核心代码如下
```javascript
function pushEffect(
hookFlags: Flags,
create: EffectCallback | void,
destroy: EffectCallback | void,
deps: HookDeps
): Effect {
const effect: Effect = {
tag: hookFlags,
create,
destroy,
deps,
next: null
};
const fiber = currentlyRenderingFiber as FiberNode;
const updateQueue = fiber.updateQueue as FCUpdateQueue;
if (updateQueue === null) {
// 创建一个新的 effect 循环
const updateQueue = createFCUpdateQueue();
// 将新创建的循环队列赋值给当前的 fiber
fiber.updateQueue = updateQueue;
// 由于是循环链表,链表为空的情况下,当前节点的下一个节点指向自己,next 指针指向自己
effect.next = effect;
// lastEffect 指向最后一个节点。有了 lastEffect 就可以很方便的找到第一个节点。只一个节点的情况下,lastEffect 就是自己
updateQueue.lastEffect = effect;
} else {
// 插入effect
const lastEffect = updateQueue.lastEffect;
if (lastEffect === null) {
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
}
}
return effect;
}
```
其中会调用 createUpdateQueue 方法。用于创建一个循环链表用于存储 effect
```javascript
function createFCUpdateQueue() {
const updateQueue = createUpdateQueue() as FCUpdateQueue;
updateQueue.lastEffect = null;
return updateQueue;
}
```
如果有多个 effect,则都会被添加到循环链表中,产生类似右侧的结构: effect1 -> effect2 -> effect1
### useEffect 回调的执行时机
- **渲染阶段收集副作用**
- **提交阶段执行副作用**
针对上面的 Demo 逐步进行分析:
#### 1. 渲染阶段(Reconciliation Phase):收集副作用,构建循环链表
当组件首次渲染时,React 会遍历组件树(从父到子),为每个组件创建 Fiber 节点,并收集其内部的 useEffect 副作用,存储到 Fiber 节点的副作用队列(updateQueue)中,队列底层是 循环链表(如之前分析的 pushEffect 逻辑)。
- 父组件 A 先进入渲染阶段:解析 A 组件的 useEffect 时,React 会调用 pushEffect 函数,将该 useEffect 的回调(包含 effect1、effect2 调用)封装为一个 Effect 对象,添加到 A 的 Fiber 节点的副作用队列(循环链表)中。此时 A 的队列中只有一个 Effect 节点(指向自身的循环链表)。
- 子组件 B 后进入渲染阶段:由于 B 是 A 的子组件,React 会先完成 A 的渲染框架,再递归渲染 B。解析 B 组件的 useEffect 时,同样通过 pushEffect 创建 Effect 对象,添加到 B 的 Fiber 节点的副作用队列(循环链表)中。
QA:假设一个页面存在3个自定义组件,每个组件内部存在一些 Dom 节点,那么会创建多少个 Fiber 节点,存在多少个 Fiber Tree?
#### 2. 提交阶段(Commit Phase):遍历 Fiber 树,执行副作用
渲染阶段完成后,React 进入提交阶段,此时会遍历整个 Fiber 树,按 “先子后父” 的顺序执行所有收集到的副作用(useEffect 的回调函数)。
递归的“归”阶段。Fiber Tree 的 DFS 过程。
##### 为什么是 “先子后父”?
组件挂载的逻辑是 “父组件挂载依赖子组件挂载完成”(父组件的 DOM 节点需要包含子组件的 DOM 节点)。因此,只有子组件完全挂载后,父组件才算真正挂载完成。副作用 (如 DOM 操作、数据请求)需要在组件挂载完成后执行,因此子组件的副作用先于父组件执行。
具体执行顺序:
- 遍历到 B 的 Fiber 节点,取出其副作用队列(循环链表)中的 Effect 对象,执行其 create 回调(即 useEffect 的回调),依次调用 effect3()、effect4(),输出 Effect 3、Effect 4。
- 遍历到 A 的 Fiber 节点,取出其副作用队列中的 Effect 对象,执行其 create 回调,依次调用 effect1()、effect2(),输出 Effect 1、Effect 2。
几个关键函数:
- flushPassiveEffects
- commitHookEffectList
```javascript
export function commitHookEffectListCreate(flags: Flags, lastEffect: Effect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const create = effect.create;
if (typeof create === 'function') {
effect.destroy = create();
}
});
}
```
```javascript
function commitHookEffectList(
flags: Flags,
lastEffect: Effect,
callback: (effect: Effect) => void
) {
let effect = lastEffect.next as Effect;
do {
if ((effect.tag & flags) === flags) {
callback(effect);
}
effect = effect.next as Effect;
} while (effect !== lastEffect.next);
}
```
pendingPassiveEffects
在 commitRoot 方法中不断调用会走到 commitPassiveEffect 方法中。
```javascript
function commitRoot(root: FiberRootNode) {
// ...
if (subtreeHasEffect || rootHasEffect) {
// beforeMutation
// mutation Placement
commitMutationEffects(finishedWork, root);
root.current = finishedWork;
// 阶段3/3:Layout
commitLayoutEffects(finishedWork, root);
} else {
root.current = finishedWork;
}
}
function commitPassiveEffect(
fiber: FiberNode,
root: FiberRootNode,
type: keyof PendingPassiveEffects
) {
// update unmount
if (
fiber.tag !== FunctionComponent ||
(type === 'update' && (fiber.flags & PassiveEffect) === NoFlags)
) {
return;
}
const updateQueue = fiber.updateQueue as FCUpdateQueue;
if (updateQueue !== null) {
if (updateQueue.lastEffect === null && __DEV__) {
console.error('当FC存在PassiveEffect flag时,不应该不存在effect');
}
root.pendingPassiveEffects[type].push(updateQueue.lastEffect as Effect);
}
}
```
可以看到 React 在 commitRoot 提交阶段会遍历所有收集到的副作用 (useEffect 属于此类),
**root.pendingPassiveEffects[type].push(updateQueue.lastEffect as Effect)** 通过这段代码,React 会将每个组件的副作用循环链表的“入口节点”(updateQueuee.lastEffect) 推入全局队列,而收集顺序是从 Fiber 叶子节点到父 Fiber 节点(DFS)
- 遍历到 BComponent 的 Fiber 节点时:
- 取 BComponent.updateQueue.lastEffect(即 EffectB,B 的循环链表入口节点)
- 将 EffectB 推入 root.pendingPassiveEffects.mount 数组。此时全局的队列为:[EffectB]
- 遍历到 AComponent 的 Fiber 节点时:
- 取 AComponent.updateQueue.lastEffect(即 EffectA,A 的循环链表入口节点)
- 将 EffectA 推入 root.pendingPassiveEffects.mount 数组。此时全局的队列为:[EffectB、EffectA]
在 commitRoot 阶段,会调用 `flushPassiveEffects` 方法。
```javascript
function commitRoot(root: FiberRootNode) {
// ...
if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
) {
if (!rootDoesHasPassiveEffects) {
rootDoesHasPassiveEffects = true;
// 调度副作用
scheduleCallback(NormalPriority, () => {
// 执行副作用
flushPassiveEffects(root.pendingPassiveEffects);
return;
});
}
}
// ...
}
```
flushPassiveEffects 方法实现如下
```javascript
function flushPassiveEffects(pendingPassiveEffects: PendingPassiveEffects) {
let didFlushPassiveEffect = false;
pendingPassiveEffects.unmount.forEach((effect) => {
didFlushPassiveEffect = true;
commitHookEffectListUnmount(Passive, effect);
});
pendingPassiveEffects.unmount = [];
pendingPassiveEffects.update.forEach((effect) => {
didFlushPassiveEffect = true;
commitHookEffectListDestroy(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update.forEach((effect) => {
didFlushPassiveEffect = true;
commitHookEffectListCreate(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update = [];
flushSyncCallbacks();
return didFlushPassiveEffect;
}
```
可以看到内部分别处理了 pendingPassiveEffects.unmount、pendingPassiveEffects.update 托管的 effect。
```javascript
export function commitHookEffectListCreate(flags: Flags, lastEffect: Effect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const create = effect.create;
if (typeof create === 'function') {
effect.destroy = create();
}
});
}
```
通过 commitHookEffectListCreate 的实现可以知道,内部是封装调用了 commitHookEffectList 方法。
```javascript
function commitHookEffectList(
flags: Flags,
lastEffect: Effect,
callback: (effect: Effect) => void
) {
let effect = lastEffect.next as Effect;
do {
if ((effect.tag & flags) === flags) {
callback(effect);
}
effect = effect.next as Effect;
} while (effect !== lastEffect.next);
}
```
commitHookEffectList 的核心职责是:
- 遍历 effect 循环链表,根据 flags 筛选出需要处理的 effect
- 然后调用 callback 对这些 effect 执行具体操作(比如执行 effect 的销毁函数 destroy 或创建函数 create)
它是 React 处理 Hook 副作用的「通用遍历器」,在不同阶段(如 layout 阶段处理 useLayoutEffect,passive 阶段处理 useEffect)会被传入不同的 flags 和 callback,以实现对不同类型副作用的精准处理。
QA:flushPassiveEffects 源码中,在执行 pendingPassiveEffects.update 相关的 effect 的时候,会先调用 destory,再去执行 create,为什么要这么做?
核心是**保证副作用的正确性、避免资源泄漏,以及确保副作用与当前组件状态的一致性。**
本质原因是:副作用的生命周期同步问题。React 的 effect 本质是「与组件状态关联的副作用」,它会随着组件的渲染(或依赖变化)而更新。当组件重新渲染(或依赖项变化)时,旧的 effect 可能已经基于「旧的状态 /props」,如果不先销毁旧的副作用,直接创建新的副作用,会导致:
- 旧的副作用与新状态的冲突:旧的副作用可能还在使用旧的 State,而新的副作用基于新的 State,两者并存会导致逻辑混乱(比如重复监听、数据不一致)
- 资源泄漏:如果副作用涉及资源占用(如事件监听、定时器、网络连接等),不销毁旧的副作用会导致这些资源无法释放,长期积累会引发**内存泄漏或性能问题**
对于 useEffect(异步执行的 effect),虽然执行时机在浏览器绘制后,但同样需要先销毁旧的副作用(基于旧状态),再创建新的(基于新状态),否则会导致副作用与组件当前状态脱节
举个例子:
```javascript
function TimerDemo() {
// 计数器状态(会影响UI)
const [count, setCount] = useState(0);
// 定时器间隔(用户可修改)
const [delay, setDelay] = useState(1000); // 初始1秒
useEffect(() => {
// 创建定时器:每隔delay毫秒更新一次count
const timer = setInterval(() => {
// 用函数式更新确保获取最新的count(避免闭包问题)
setCount(prevCount => prevCount + 1);
}, delay);
// 销毁函数:清除当前定时器
return () => {
clearInterval(timer);
};
}, [delay]); // 依赖delay:当delay变化时,重新执行effect
return (
当前计数:{count}
定时器间隔:{delay}ms
setDelay(Number(e.target.value))}
/>
修改输入框可改变间隔,观察计数变化
);
}
```
effect 中包含定时器,依赖于 delay 状态。input 输入框内容变化会改变 delay 的值,从而重新创建 timer。如果 React 官方的实现里面,没有在 effect 的执行前,把 effect.destory 先执行,再去执行 effect.create 就可能存在2个 timer,timer 回调里会修改 count,造成 UI 的错乱。
## React 并发更新