前言
本文章用自己的思路加上 AI 润色写成。如有错误和建议,欢迎指出。
默认你已经理解了:
- Virtual DOM
- JSX
什么是 Fiber
在编译阶段,我们写的 JSX 会被 React 源码里的 jsx 函数、reactElement 函数处理成一个个 ReactElement 对象,里面简单记录了元素的一些信息,例如:
js
{
type: 'h1',
key: null,
ref: null,
props: {
className: 'greeting',
children: 'Hello, world!'
},
// ... 其他内部属性
}
它非常轻量,不包含任何实例、状态或真实的 DOM 节点。它只是告诉 React:"请帮我创建一个 h1 标签,它的 className 是 'greeting',内容是 'Hello, world!'"。
在浏览器运行时,每一个 ReactElement 就会被转换成 Fiber 节点。它也是一个对象,但是它更加复杂,里面有很多重要属性。
了解每个重要的属性,对我们以后去理解 React 原理有很大的帮助。
Fiber 节点的 TypeScript 类型定义( AI 生成):
ts
// ==================== 辅助类型定义 ====================
// 1. WorkTag: 标识 Fiber 节点的类型(函数组件、类组件、DOM 节点等)
// 在源码中这是一个枚举,这里用类型别名简化
type WorkTag = number;
const FunctionComponent = 0;
const ClassComponent = 1;
const IndeterminateComponent = 2; // 判断不出是函数还是类组件
const HostRoot = 3; // 根节点,通过 createRoot 创建
const HostPortal = 4;
const HostComponent = 5; // DOM 元素,如 'div'
const HostText = 6; // 文本节点
const Fragment = 7;
// ...还有很多其他类型
// 2. Flags: 副作用标记,也是一个位掩码
// 旧版本中叫 effectTag
type Flags = number;
const NoFlags = 0b000000000000000000000;
const Placement = 0b000000000000000000010; // 插入
const Update = 0b000000000000000000100; // 更新
const Deletion = 0b000000000000000001000; // 删除
const ChildDeletion = 0b000000000000000010000; // 子树删除
const Ref = 0b000000000000001000000; // ref 变更
// ...还有很多其他标记
// 3. Lanes: 优先级模型,也是位掩码
type Lanes = number;
const NoLanes = 0b0000000000000000000000000000000;
const SyncLane = 0b0000000000000000000000000000001; // 同步任务,最高优先级
// ...有很多优先级车道
// 4. React 元素、组件、Ref 等的简化类型
type ReactElement = any; // 简化
type ReactNode = any; // 简化
type Ref = any; // 简化
type Type = any; // 组件类型或 DOM 标签字符串
type Key = string | null;
type Props = any; // 简化
type State = any; // 简化
type UpdateQueue<any> = any; // 简化,内部存储 state 更新
// ==================== 核心 Fiber 接口 ====================
interface Fiber {
// 1. 核心标识信息
// --------------------
/** 标记 Fiber 的类型,如 FunctionComponent, HostComponent 等 */
tag: WorkTag;
/** React 元素中的 key,用于 diff 算法 */
key: Key;
/** 元素的类型,对于 DOM 元素是 'div' 等字符串,对于组件是组件函数/类本身 */
elementType: Type;
/** 与 elementType 类似,但在某些情况下(如 React.memo)会不同 */
type: Type;
/** 在当前父节点下的子节点列表中的索引 */
index: number;
// 2. Fiber 树结构(链表结构)
// --------------------
/** 指向父节点 Fiber */
return: Fiber | null;
/** 指向第一个子节点 Fiber */
child: Fiber | null;
/** 指向下一个兄弟节点 Fiber */
sibling: Fiber | null;
// 3. 状态与数据
// --------------------
/** 当前组件的 props,在 reconcile 阶段结束后会变成 memoizedProps */
pendingProps: Props;
/** 上一次渲染时使用的 props,用于性能优化(如 React.memo)的对比 */
memoizedProps: Props;
/** 上一次渲染后的 state */
memoizedState: State;
/** 存储 state 更新的队列(来自 setState 或 useState 的 dispatch) */
updateQueue: UpdateQueue<State> | null;
/** ref 引用 */
ref: Ref | null;
// 4. 副作用系统
// --------------------
/** 记录当前 Fiber 需要执行的副作用(增、删、改) */
flags: Flags;
/** 记录子树中存在的副作用,用于快速判断是否需要遍历子树 */
subtreeFlags: Flags;
/** 一个数组,存放了本次更新需要被删除的子节点 */
deletions: (Fiber | null)[] | null;
// 5. 调度与优先级
// --------------------
/** 当前 Fiber 自身的更新优先级 */
lanes: Lanes;
/** 当前 Fiber 的子树中存在的最高优先级 */
childLanes: Lanes;
/** 用于表示当前 Fiber 所在树的渲染模式(如并发模式、严格模式) */
mode: number;
// 6. 双缓冲指针
// --------------------
/** 指向内存中正在构建的另一个 Fiber 树的对应节点 */
alternate: Fiber | null;
// 7. 宿主相关
// --------------------
/**
* 指向与该 Fiber 关联的"真实"节点。
* - 对于 HostComponent (DOM 元素),它是真实的 DOM 节点。
* - 对于 ClassComponent,它是类的实例。
* - 对于 FunctionComponent,它通常是 null。
*/
stateNode: any;
}
重要属性介绍
tag
简单来说,tag 是一个数字常量(0, 1, 2, 3...) ,它的作用是标识当前 Fiber 节点代表的组件类型。
React 会根据不同的 tag 类型对其做对应的处理。
下面随便列出几个 tag 看看是什么样子的(不用刻意记忆,知道咋回事就行了):
tag 常量名 |
数值 | 对应的 JSX/React 元素 | 描述 |
|---|---|---|---|
| FunctionComponent | 0 | function MyComponent() {} |
函数组件。最常见的组件类型之一。 |
| ClassComponent | 1 | class MyClassComponent {} |
类组件。React 16.8 之前的主流。 |
| IndeterminateComponent | 2 | function MyComponent() {} |
不确定组件。在首次渲染时,React 还不知道一个函数是函数组件还是作为类组件使用,会先打上这个标记,后续再确定。 |
| HostRoot | 3 | ReactDOM.createRoot(rootElement) |
根节点 。整个 Fiber 树的入口,由 createRoot 创建。 |
| HostPortal | 4 | ReactDOM.createPortal(child, container) |
Portal。将子节点渲染到父组件 DOM 层级之外的节点。 |
| HostComponent | 5 | <div>, <span>, <button> |
宿主组件(DOM 元素)。代表所有原生 HTML/SVG 等标签。 |
| HostText | 6 | "Some text" |
文本节点。JSX 中的字符串内容。 |
| ... | ... | ... | ... |
stateNode
stateNode 是一个引用,指向与当前 Fiber 节点关联的、具体的、可操作的实例。其具体类型由 Fiber 的 tag 属性决定。
例如,当 Fiber 节点的 tag 为 HostComponent (值为 5) 时, stateNode 指向真实的 DOM 节点:
js
// 对于 <div>Hello</div> 这样的 JSX 元素
const fiberNode = {
tag: 5, // HostComponent
type: 'div',
stateNode: document.createElement('div') // 真实的 DOM 元素
};
memoizedState
memoizedState 是一个 单向链表 ,链表上的每个节点对应一个 Hook。
js
function Counter() {
const [count, setCount] = useState(0); // 第一个 Hook
const [name, setName] = useState('React'); // 第二个 Hook
const ref = useRef(null); // 第三个 Hook
return <div>{count}: {name}</div>;
}
// 对应的 memoizedState 链表结构
fiber.memoizedState = {
// 第一个节点: useState(0)
memoizedState: 0, // 当前状态值
queue: { // 更新队列
pending: null,
dispatch: setCount // 更新函数
},
next: { // 指向下一个 Hook
// 第二个节点: useState('React')
memoizedState: 'React',
queue: {
pending: null,
dispatch: setName
},
next: { // 指向下一个 Hook
// 第三个节点: useRef(null)
memoizedState: { current: null },
queue: null,
next: null // 链表末尾
}
}
};
当用户触发了 setCount(1) 时:
- React 创建一个 update 对象:
{ action: 1, lane: ... } - 将 update 加入 count Hook 的
queue.pending链表 - 调度一次重新渲染
重新渲染时:
- React 按顺序遍历 memoizedState 链表
- 对于第一个 Hook (useState):
- 从 queue.pending 中取出所有 update
- 计算新状态: 0 + 1 = 1
- 更新 memoizedState: 0 -> 1
这也是为什么 Hooks 不能放在条件语句或循环中。因为 React 依赖于 memoizedState 链表的顺序来正确地调用和更新每个 Hook。例如:
js
// 正确的 Hook 使用方式
function Counter() {
const [count, setCount] = useState(0); // 总是第一个 Hook
const [name, setName] = useState('React'); // 总是第二个 Hook
const ref = useRef(null); // 总是第三个 Hook
// 每次渲染,Hooks 都按相同顺序被调用
// React 可以通过 memoizedState 链表正确恢复每个 Hook 的状态
return <div>{count}: {name}</div>;
}
// 错误的 Hook 使用方式
function Counter() {
const [count, setCount] = useState(0);
if (someCondition) {
// 条件语句中使用 Hook 会破坏顺序一致性
// 导致 memoizedState 链表与 Hook 调用顺序不匹配
const [name, setName] = useState('React');
}
// 当 someCondition 为 false 时:
// 第一次渲染: Hook 链表有 1 个节点 (useState for count)
// 第二次渲染 (someCondition 为 true): Hook 链表有 2 个节点
// React 无法正确匹配 Hook 与其状态
return <div>{count}</div>;
}
pendingProps 与 memoizedProps
pendingProps 和 memoizedProps 是一对密切相关的属性:
- pendingProps :当前渲染周期 新接收 的 props
- memoizedProps :上一次渲染完成时 已固化 的 props
这也是判断 memo 包裹组件是否需要更新的依据:
js
// 对于 React.memo 包裹的组件
const MemoizedChild = React.memo(function Child({ count }) {
console.log('Child rendered');
return <div>Count: {count}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Update Count</button>
<button onClick={() => setOther(other + 1)}>Update Other</button>
{/* 当 other 变化但 count 不变时: */}
{/* pendingProps = { count: 0 } */}
{/* memoizedProps = { count: 0 } */}
{/* props 相同,跳过渲染 */}
<MemoizedChild count={count} />
</div>
);
}
updateQueue
updateQueue 本质上就是一个 "暂存区"或者"收件箱"。
React 把所有对状态的修改请求(比如 setState 或 setCount)先不立即执行,而是封装成一个"待办事项",放进这个收件箱里。
等时机成熟了(比如当前浏览器空闲了),React 再统一从这个收件箱里取出所有待办事项,合并计算,然后一次性地、高效地更新组件。
基本结构:
js
// updateQueue 的基本结构
const updateQueue = {
pending: null, // 指向环形链表的第一个 update
lanes: 0, // 当前队列中 update 的优先级车道
// 其他属性...
};
// update 对象的结构
const update = {
lane: 1, // 优先级车道
action: null, // 更新动作(如新状态值或函数)
next: null // 指向下一个 update,形成环形链表
};
updateQueue 使用环形链表存储 update 对象:update1 -> update2 -> update3 -> update1 (形成环)
这种结构有以下优势:
- 高效的添加操作:O(1) 时间复杂度
- 节省内存:不需要额外的指针指向链表尾部
- 便于批量处理:可以快速遍历所有更新
lanes
lanes 确实是一个使用二进制表示的优先级模型,包含 31 个不同类型的车道(优先级)。每个车道对应二进制中的一个特定位,从右到左(从低位到高位)优先级逐渐降低:
js
// 简化示例(实际有31个车道)
const SyncLane = 0b0000000000000000000000000000001; // 最高优先级
const InputContinuousLane = 0b0000000000000000000000000000100; // 高优先级
const DefaultLane = 0b0000000000000000000000000010000; // 普通优先级
const TransitionLane1 = 0b0000000000000000000001000000000; // 低优先级
const IdleLane = 0b0100000000000000000000000000000; // 最低优先级
例如:
js
// 假设一个组件同时有多种类型的更新
function MyComponent() {
const [text, setText] = useState('');
const [data, setData] = useState(null);
const [count, setCount] = useState(0);
// 用户输入 - 高优先级
const handleChange = (e) => {
setText(e.target.value); // InputContinuousLane
};
// 用户点击 - 最高优先级
const handleClick = () => {
setCount(count + 1); // SyncLane
};
// 数据加载 - 低优先级
const loadData = () => {
fetch('/api/data')
.then(res => res.json())
.then(responseData => {
startTransition(() => {
setData(responseData); // TransitionLane
});
});
};
return (
<div>
<input value={text} onChange={handleChange} />
<button onClick={handleClick}>Click: {count}</button>
<button onClick={loadData}>Load Data</button>
{data && <div>Data: {data}</div>}
</div>
);
}
// 对应 Fiber.lanes 的变化过程
// 初始: 0b0000000000000000000000000000000
// 用户输入后: 0b0000000000000000000000000000100
// 用户点击后: 0b0000000000000000000000000000101
// 数据加载后: 0b0000000000000000000001000000101
// 调度器会优先处理最右边的 1 (SyncLane)
update.lane 与 Fiber.lanes 的关系
- Fiber.lanes 表示该 Fiber 节点需要处理的 所有优先级 的组合
- 调度器 首先从 fiber.lanes 中找出 最高优先级 的 lane
- 然后处理该 Fiber 节点的 updateQueue 中 匹配或高于 这个优先级的所有 update
例子:
js
// 1. 假设一个 Fiber 节点有以下 lanes
fiber.lanes = SyncLane | DefaultLane | TransitionLane1;
// 即: 0b0000000000000000000001000010001
// 2. 调度器获取最高优先级
const nextLane = getHighestPriorityLane(fiber.lanes);
// nextLane = SyncLane (0b0000000000000000000000000000001)
// 3. 处理 updateQueue 中匹配或高于 nextLane 的所有 update
function processUpdateQueue(workInProgress, renderLanes) {
const queue = workInProgress.updateQueue;
// 遍历 updateQueue 中的所有 update
let update = queue.shared.pending;
if (update !== null) {
do {
// 检查这个 update 的 lane 是否在当前渲染 lanes 中
if (isSubsetOfLanes(renderLanes, update.lane)) {
// 处理这个 update
const action = update.action;
newState = typeof action === 'function' ? action(newState) : action;
}
update = update.next;
} while (update !== queue.shared.pending);
}
}
child, sibling, return
child 、 sibling 和 return 是 Fiber 架构中三个至关重要的指针,它们共同构成了 Fiber 树的链表结构,是 React 实现可中断渲染的基础。
这三个指针的作用如下:
- child : 指向该节点的第一个子节点
- sibling : 指向该节点的下一个兄弟节点
- return : 指向该节点的父节点
传统递归遍历一旦开始就无法中断,直到调用栈耗尽。Fiber 的链表结构使遍历变成一个循环过程,可以在任何 Fiber 节点后暂停。当渲染被中断后,React 只需要保存当前正在处理的 Fiber 节点引用。当浏览器空闲时,可以从这个节点恢复工作。
flags, subtreeFlags, deletions
flags 是 Fiber 节点上的一个属性,用于记录当前节点需要执行的副作用(DOM操作)。它是一个二进制位掩码,每个特定位代表一种特定的副作用类型。
常见的flags标记包括:
js
- NoFlags = 0b000000000000000000000 - 无副作用
- Placement = 0b000000000000000000010 - 插入操作(新增节点)
- Update = 0b000000000000000000100 - 更新操作(修改节点)
- Deletion = 0b000000000000000001000 - 删除操作
- ChildDeletion = 0b000000000000000010000 - 子树删除
- Ref = 0b000000000000001000000 - ref变更
在React中,副作用是组件渲染之外的所有操作,包括:
- DOM操作 :插入、更新、删除DOM元素
- Ref操作 :ref的赋值和变更
- 生命周期/Hook :useEffect等
- 数据获取 :API调用、订阅等
- 定时器 :setTimeout、setInterval等
subtreeFlags 是Fiber节点上的另一个属性,用于记录该节点 子树中 存在的所有副作用。它也是一个二进制位掩码,通过位运算将子节点的flags聚合到父节点上。
这个属性的主要作用是 优化遍历效率 。在提交阶段,React可以通过检查 subtreeFlags 快速判断某个子树是否包含副作用,而不需要遍历整个子树。
deletions 是一个数组,存储了本次更新中需要被删除的子节点Fiber。当React在协调阶段发现某些节点需要被删除时,会将这些节点的引用添加到父节点的 deletions 数组中。