一、React Hook 的使用限制
必须在函数组件或自定义 Hook 的最顶层调用 Hooks,不能放在循环、条件语句或嵌套函数中。
- React 通过“调用顺序”来记录 Hook 状态
React 内部使用链表来存储 Hook 状态,在函数组件的 fiber 节点(FiberNode)上,有一个 memoizedState 指针,它指向一个 单向链表,链表的每个节点就是一个 Hook 对象(Hook)。
每个 Hook 节点大致长这样:
type Hook = {
memoizedState: any, // 当前的 state 或 effect 的依赖
baseState: any, // 初始状态(主要用于 useReducer)
baseQueue: any, // 更新队列(useReducer/useState 的 setXXX)
queue: any, // 链接 setState 的更新任务
next: Hook | null // 指向下一个 Hook(链表结构)
}
而 fiber.memoizedState 指向链表的头部:
fiber.memoizedState -> Hook1 -> Hook2 -> Hook3 -> null
React 渲染组件时,每调用一次 Hook,就沿着链表往下走一步。
(1)只能在函数组件顶层调用:因为React依赖调用顺序来匹配链表中的节点
(2)不能在条件语句、循环或嵌套函数中调用:这会破坏调用顺序,导致链表节点匹配错误
- 如果放在条件语句中,会破坏顺序
function MyComponent({ flag }) {
if (flag) {
const [count, setCount] = useState(0); // ❌
}
const [name, setName] = useState("A");
}
-
当
flag = true时,调用顺序是:(1).
useState(0)→count(2).
useState("A")→name -
当
flag = false时,调用顺序变成:useState("A")→ React 仍然认为这是第一个 Hook
结果:name 的状态会错乱到 count 的位置,React 内部的 hooks 链表错位,渲染逻辑全乱。
- 如果放在循环中,同样会错乱
function MyComponent({ list }) {
for (let i = 0; i < list.length; i++) {
const [value, setValue] = useState(i); // ❌
}
}
- 如果第一次渲染
list.length = 3,React 内部会创建 3 个useState。 - 下一次渲染
list.length = 5,React 需要创建 5 个useState。 - 顺序和数量都变了,React 内部的状态链条直接乱掉。
- 如果放在嵌套函数里,也会破坏一致性
function MyComponent() {
function inner() {
const [value, setValue] = useState(0); // ❌
}
inner();
}
- React 在进入组件渲染时,会按照顺序一层层执行 Hook。
- 但这里
useState是在运行时的inner()才调用的,React 无法在组件“顶层”预测到底会不会执行。 - 这会导致 Hook 的注册和执行不稳定,破坏调用顺序。
- 为什么是链表?
- 链表便于动态扩展:不同组件可能有不同数量、不同类型的 Hook。
- 插入/删除 Hook 方便,比如热更新或 fiber 切换时,可以很快重连。
- 避免数组重新分配、下标错位的问题。
二、React Diff 算法详解
1. 概述
React的Diff算法是Virtual DOM机制的核心部分,负责比较新旧Virtual DOM树的差异,并计算出最小的DOM操作来更新真实DOM。这个算法的设计目标是在保证正确性的前提下,尽可能提高性能。
2. 传统Diff算法的复杂度问题
传统的树形结构diff算法的时间复杂度为O(n³),其中n是树中节点的数量。这是因为:
- 需要遍历两棵树的每个节点 (O(n²))
- 对每个节点需要计算编辑距离 (O(n))
对于包含1000个元素的页面,这意味着需要进行10亿次比较,这在实际应用中是无法接受的。
3. React Diff算法的三大策略
React基于以下三个假设,将O(n³)复杂度转换为O(n):
Tree Diff (层级遍历)
↓
Component Diff (组件类型判断)
↓
Element Diff (具体元素比较)
3.1 策略一:Tree Diff - 分层比较
假设:Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计
- 只会对相同层级的节点进行比较
- 如果某个节点不存在了,该节点及其子节点会被完全删除,不会用于进一步比较
- 如果发现跨层级移动,不会进行移动操作,而是删除重建
旧树: 新树:
A A
/ \ / \
B C D C
/ / \
D B E
在上面的例子中,节点D从C的子节点变成了A的子节点。React会:
- 删除C下的D
- 在A下创建新的D及其子树
3.2 策略二:Component Diff - 组件比较
假设:拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构
React对组件的比较策略:
- 同类型组件:按照原策略继续比较Virtual DOM树
- 不同类型组件:判定为dirty component,替换整个组件下的所有子节点
- 同类型组件,Virtual DOM没有变化:React允许用户通过
shouldComponentUpdate()或React.memo()来判断该组件是否需要diff
// 不同类型组件的例子
// 旧组件
<div>
<ComponentA />
</div>
// 新组件
<div>
<ComponentB />
</div>
// React会删除ComponentA,重新创建ComponentB
3.3 策略三:Element Diff - 元素比较
假设:对于同一层级的一组子节点,可以通过唯一的key来区分
对于同一层级的子节点,React提供了三种节点操作:
- INSERT_MARKUP:插入新节点
- MOVE_EXISTING:移动现有节点
- REMOVE_NODE:删除节点
Key的重要性
// 没有key的情况
<ul>
<li>张三</li>
<li>李四</li>
<li>王五</li>
</ul>
// 在开头插入新元素
<ul>
<li>赵六</li> {/* 会被认为是修改:张三->赵六 */}
<li>张三</li> {/* 会被认为是修改:李四->张三 */}
<li>李四</li> {/* 会被认为是修改:王五->李四 */}
<li>王五</li> {/* 会被认为是新插入 */}
</ul>
// 有key的情况
<ul>
<li key="zhang">张三</li>
<li key="li">李四</li>
<li key="wang">王五</li>
</ul>
// 在开头插入新元素
<ul>
<li key="zhao">赵六</li> {/* 新插入 */}
<li key="zhang">张三</li> {/* 位置移动,但元素复用 */}
<li key="li">李四</li> {/* 位置移动,但元素复用 */}
<li key="wang">王五</li> {/* 位置移动,但元素复用 */}
</ul>
4. Diff算法的具体执行流程
4.1 树的遍历方式
React采用深度优先遍历的方式对比两棵Virtual DOM树:
A
/ \
B C
/ / \
D E F
遍历顺序:A -> B -> D -> C -> E -> F
4.2 节点比较流程
// 伪代码
function diff(oldNode, newNode) {
// 1. 如果新节点不存在,删除旧节点
if (newNode === null) {
return { type: 'REMOVE', oldNode };
}
// 2. 如果旧节点不存在,插入新节点
if (oldNode === null) {
return { type: 'INSERT', newNode };
}
// 3. 如果节点类型不同,替换节点
if (oldNode.type !== newNode.type) {
return { type: 'REPLACE', oldNode, newNode };
}
// 4. 如果是文本节点且内容不同,更新文本
if (isTextNode(oldNode) && oldNode.text !== newNode.text) {
return { type: 'TEXT', newText: newNode.text };
}
// 5. 如果是元素节点,比较属性和子节点
if (isElementNode(oldNode)) {
const propsDiff = diffProps(oldNode.props, newNode.props);
const childrenDiff = diffChildren(oldNode.children, newNode.children);
return {
type: 'UPDATE',
propsDiff,
childrenDiff
};
}
}
4.3 子节点的Diff算法
对于子节点数组的比较,React使用了一个优化的算法:
(1)第一阶段:从左到右遍历
- 比较新老节点数组从头开始的相同key节点
- 如果key相同,更新节点内容,继续遍历
- 如果key不同或遍历到数组末尾,停止遍历
(2)第二阶段:从右到左遍历
- 比较新老节点数组从尾部开始的相同key节点
- 如果key相同,更新节点内容,继续遍历
- 如果key不同或遍历到数组开头,停止遍历
(3)第三阶段:处理简单情况
- 如果老节点遍历完:批量插入剩余的新节点
- 如果新节点遍历完:批量删除剩余的老节点
- 如果都有剩余:进入第四阶段的复杂处理
(4)第四阶段:处理复杂的移动、新增、删除
-
建立映射表:将剩余老节点的key和索引建立Map映射
-
标记操作类型:遍历剩余新节点,在Map中查找对应老节点
- 找到:标记为更新或移动
- 未找到:标记为新增
-
检测移动:通过老节点索引的递增性判断是否需要移动
-
优化移动:使用最长递增子序列算法计算最少的移动操作
-
执行操作:从后往前执行插入、移动、删除操作
这种设计的核心优化点:
- 双端比较快速处理头尾相同的节点
- 最长递增子序列最小化DOM移动次数
- Map查找将查找复杂度从O(n)降为O(1)
- 从后往前操作避免DOM位置变化的影响
// 子节点Diff伪代码
function diffChildren(oldChildren, newChildren) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newEndIdx = newChildren.length - 1;
// 第一轮遍历:从左到右比较
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
const oldNode = oldChildren[oldStartIdx];
const newNode = newChildren[newStartIdx];
if (oldNode.key === newNode.key) {
// key相同,更新节点
diff(oldNode, newNode);
oldStartIdx++;
newStartIdx++;
} else {
// key不同,跳出第一轮遍历
break;
}
}
// 第二轮遍历:从右到左比较
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
const oldNode = oldChildren[oldEndIdx];
const newNode = newChildren[newEndIdx];
if (oldNode.key === newNode.key) {
// key相同,更新节点
diff(oldNode, newNode);
oldEndIdx--;
newEndIdx--;
} else {
// key不同,跳出第二轮遍历
break;
}
}
// 第三步:处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 老节点已经遍历完,剩余的新节点需要插入
const before = newChildren[newEndIdx + 1];
for (let i = newStartIdx; i <= newEndIdx; i++) {
insertNode(newChildren[i], before);
}
} else if (newStartIdx > newEndIdx) {
// 新节点已经遍历完,剩余的老节点需要删除
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
removeNode(oldChildren[i]);
}
} else {
// 都有剩余节点,需要处理移动、新增、删除的复杂情况
handleMovingNodes(oldChildren, newChildren, oldStartIdx, newStartIdx, oldEndIdx, newEndIdx);
}
}
// 处理复杂移动逻辑的详细实现
function handleMovingNodes(oldChildren, newChildren, oldStartIdx, newStartIdx, oldEndIdx, newEndIdx) {
// 第一步:创建剩余老节点的key映射表
const oldKeyToIdx = new Map();
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldChildren[i].key;
if (key != null) {
oldKeyToIdx.set(key, i);
}
}
// 第二步:遍历剩余新节点,标记移动和新增
const toBePatched = newEndIdx - newStartIdx + 1; // 需要处理的新节点数量
const newIndexToOldIndexMap = new Array(toBePatched).fill(-1); // -1表示新增节点
let moved = false; // 是否需要移动
let maxNewIndexSoFar = 0; // 已遍历节点在老数组中的最大索引
let patched = 0; // 已处理的节点数量
// 遍历剩余的新节点
for (let newIdx = newStartIdx; newIdx <= newEndIdx; newIdx++) {
const newChild = newChildren[newIdx];
if (patched >= toBePatched) {
// 新节点已经处理完,删除多余的老节点
removeNode(newChild);
continue;
}
let oldIdx;
if (newChild.key != null) {
// 有key,在映射表中查找对应的老节点
oldIdx = oldKeyToIdx.get(newChild.key);
} else {
// 没有key,线性搜索匹配的节点(性能较差)
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldChildren[i].key == null &&
isSameNodeType(oldChildren[i], newChild)) {
oldIdx = i;
break;
}
}
}
if (oldIdx === undefined) {
// 在老节点中没找到,说明是新增节点
insertNode(newChild);
} else {
// 找到了对应的老节点
newIndexToOldIndexMap[newIdx - newStartIdx] = oldIdx;
if (oldIdx >= maxNewIndexSoFar) {
// 老节点索引递增,不需要移动
maxNewIndexSoFar = oldIdx;
} else {
// 老节点索引不是递增的,需要移动
moved = true;
}
// 更新节点内容
diff(oldChildren[oldIdx], newChild);
patched++;
}
}
// 第三步:生成移动操作
if (moved) {
// 计算最长递增子序列,这些节点不需要移动
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
let j = increasingNewIndexSequence.length - 1;
// 从后往前处理,避免位置变化影响
for (let i = toBePatched - 1; i >= 0; i--) {
const nextIndex = newStartIdx + i;
const nextChild = newChildren[nextIndex];
const anchor = nextIndex + 1 < newChildren.length
? newChildren[nextIndex + 1]
: null;
if (newIndexToOldIndexMap[i] === -1) {
// 新增节点
insertNode(nextChild, anchor);
} else if (j < 0 || i !== increasingNewIndexSequence[j]) {
// 需要移动的节点
moveNode(nextChild, anchor);
} else {
// 在最长递增子序列中,不需要移动
j--;
}
}
}
}
// 计算最长递增子序列(用于优化移动操作)
function getSequence(arr) {
const n = arr.length;
const result = [0];
const p = new Array(n);
for (let i = 1; i < n; i++) {
if (arr[i] === -1) continue;
const last = result[result.length - 1];
if (arr[i] > arr[last]) {
p[i] = last;
result.push(i);
} else {
// 二分查找
let left = 0, right = result.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (arr[result[mid]] < arr[i]) {
left = mid + 1;
} else {
right = mid;
}
}
if (arr[i] < arr[result[left]]) {
if (left > 0) p[i] = result[left - 1];
result[left] = i;
}
}
}
// 回溯构建序列
let i = result.length;
let u = result[i - 1];
while (i-- > 0) {
result[i] = u;
u = p[u];
}
return result;
}
// 辅助函数
function isSameNodeType(a, b) {
return a.type === b.type;
}
function insertNode(node, before = null) {
// 实际DOM插入操作
console.log(`插入节点: ${node.key}`, before ? `在${before.key}之前` : '在末尾');
}
function removeNode(node) {
// 实际DOM删除操作
console.log(`删除节点: ${node.key}`);
}
function moveNode(node, before = null) {
// 实际DOM移动操作
console.log(`移动节点: ${node.key}`, before ? `到${before.key}之前` : '到末尾');
}
5. React 18中的Diff优化
React 18引入了并发特性,对Diff算法也进行了优化:
5.1 可中断的Diff
- Diff过程可以被中断,让出主线程给更重要的任务
- 通过时间切片(Time Slicing)机制,防止长时间的Diff阻塞页面
5.2 优先级调度
- 不同的更新有不同的优先级
- 高优先级的更新可以中断低优先级的Diff过程
5.3 Lanes模型
- 使用Lanes模型来管理不同优先级的更新
- 更细粒度的批处理和调度
// React 18中的优先级示例
function updateWithPriority(priority, update) {
const currentLanes = getCurrentLanes();
const updateLane = priority === 'urgent' ? SyncLane : TransitionLane;
if (updateLane > currentLanes) {
// 中断当前diff,处理高优先级更新
interruptCurrentWork();
scheduleWork(updateLane, update);
} else {
// 加入当前批次
addToBatch(update);
}
}
6. 性能优化建议
6.1 正确使用key
// ❌ 不要使用数组索引作为key
{items.map((item, index) =>
<Item key={index} data={item} />
)}
// ✅ 使用稳定、唯一的标识符
{items.map(item =>
<Item key={item.id} data={item} />
)}
6.2 避免不必要的组件重新渲染
// 使用React.memo
const MyComponent = React.memo(function MyComponent({ name }) {
return <div>{name}</div>;
});
// 使用useMemo和useCallback
function Parent() {
const expensiveValue = useMemo(() => computeExpensiveValue(), [dep]);
const stableCallback = useCallback(() => doSomething(), [dep]);
return <Child value={expensiveValue} onClick={stableCallback} />;
}
6.3 合理的组件拆分
// ❌ 过大的组件会导致不必要的diff
function LargeComponent() {
return (
<div>
<Header />
<ExpensiveList items={items} />
<Footer />
</div>
);
}
// ✅ 将不变的部分抽取为独立组件
const Header = React.memo(function Header() {
return <header>...</header>;
});
const Footer = React.memo(function Footer() {
return <footer>...</footer>;
});
7. 调试和性能监控
7.1 React DevTools
- 使用Profiler面板查看组件渲染时间
- 查看组件重新渲染的原因
7.2 性能监控代码
// 测量组件渲染时间
function MyComponent() {
const renderStartTime = performance.now();
useEffect(() => {
const renderEndTime = performance.now();
console.log(`Render time: ${renderEndTime - renderStartTime}ms`);
});
return <div>...</div>;
}
8. 总结
React的Diff算法通过三大策略将复杂度从O(n³)降低到O(n):
- Tree Diff:只比较同层级节点
- Component Diff:同类型组件生成相似树结构的假设
- Element Diff:通过key标识同层级节点
这种设计在实际的Web应用场景下表现优异,但也需要开发者理解其原理,合理使用key,避免跨层级移动,并通过适当的组件设计来配合算法发挥最佳性能。
React 18的并发特性进一步增强了Diff算法的能力,通过可中断的渲染和优先级调度,让应用能够更好地响应用户交互,提供更流畅的用户体验。
三、React Fiber架构详解
1. 什么是Fiber
Fiber是React 16引入的全新协调(reconciliation)引擎,它是React核心算法的完全重写。Fiber的设计目标是增强React在动画、布局、手势等场景下的适应性,实现增量式渲染,将渲染工作分解成多个片段,并将其分散到多个帧中执行。
2. 为什么需要Fiber
2.1 旧版React的问题
在React 15及之前的版本中,协调过程是同步且递归的,存在以下问题:
- 阻塞渲染:一旦开始更新,就必须完成整个组件树的遍历
- 无法中断:长时间运行的任务会阻塞主线程
- 用户体验差:导致页面卡顿,特别是在复杂应用中
- 优先级缺失:无法区分紧急更新和普通更新
2.2 Fiber解决的核心问题
- 可中断性:将大任务分解成小任务,可以被中断和恢复
- 优先级调度:不同类型的更新具有不同的优先级
- 并发渲染:支持时间切片,充分利用浏览器的空闲时间
- 错误边界:更好的错误处理机制
3. Fiber的核心概念
3.1 什么是Fiber节点
Fiber节点是一个JavaScript对象,代表了组件、DOM节点或其他React元素的工作单元。每个Fiber节点包含以下关键信息:
{
// 节点类型信息
tag: WorkTag, // 节点类型(函数组件、类组件、DOM节点等)
type: any, // 组件类型或DOM标签
key: null | string, // React key
// 节点关系
child: Fiber | null, // 第一个子节点
sibling: Fiber | null, // 下一个兄弟节点
return: Fiber | null, // 父节点
index: number, // 在兄弟节点中的索引
// 状态和属性
pendingProps: any, // 新的props
memoizedProps: any, // 上一次渲染的props
memoizedState: any, // 上一次渲染的state
updateQueue: UpdateQueue | null, // 更新队列
// 副作用
flags: Flags, // 副作用标记
subtreeFlags: Flags, // 子树副作用标记
deletions: Array<Fiber> | null, // 需要删除的子节点
// 调度相关
lanes: Lanes, // 优先级信息
childLanes: Lanes, // 子节点的优先级
// 双缓存
alternate: Fiber | null, // 对应的另一个Fiber节点
}
3.2 Fiber树结构
Fiber采用链表结构来表示组件树:
App
/ \
Header Main
/ | \
Logo Nav Content
在Fiber中表示为:
- 每个节点只有一个
child指针指向第一个子节点 - 兄弟节点通过
sibling指针连接 - 所有子节点都有
return指针指向父节点
4. 双缓存机制
4.1 工作原理
React Fiber使用双缓存技术,维护两个Fiber树:
- current树:当前显示在屏幕上的Fiber树
- workInProgress树:正在构建的新Fiber树
4.2 构建过程
- 创建workInProgress树:基于current树创建新的工作树
- 协调过程:在workInProgress树上进行diff和更新
- 提交阶段:将workInProgress树应用到DOM
- 树切换:workInProgress树变成新的current树
// 双缓存节点关系
currentFiber.alternate = workInProgressFiber;
workInProgressFiber.alternate = currentFiber;
5. Fiber的工作循环
5.1 渲染阶段(Render Phase)
这个阶段是可中断的,主要工作包括:
function workLoopConcurrent() {
// 当有工作要做且没有被中断时继续工作
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// 处理当前节点
let next = beginWork(current, unitOfWork, renderLanes);
if (next === null) {
// 如果没有子节点,完成当前节点
completeUnitOfWork(unitOfWork);
} else {
// 继续处理子节点
workInProgress = next;
}
}
5.2 提交阶段(Commit Phase)
这个阶段是不可中断的,分为三个子阶段:
- before mutation:执行DOM操作前
- mutation:执行DOM操作
- layout:DOM操作后,执行副作用
function commitRoot(root) {
// 阶段1:before mutation
commitBeforeMutationEffects(root, finishedWork);
// 阶段2:mutation
commitMutationEffects(root, finishedWork);
// 切换current指针
root.current = finishedWork;
// 阶段3:layout
commitLayoutEffects(finishedWork, root);
}
6. 优先级调度
6.1 Lane模型
Fiber使用Lane模型来管理优先级:
// 不同类型的Lane优先级
const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;
const DefaultLane = 0b0000000000000000000000000010000;
const TransitionLane = 0b0000000000000000000001000000000;
const IdleLane = 0b0100000000000000000000000000000;
6.2 调度器(Scheduler)
React使用Scheduler来实现时间切片:
// 根据优先级调度任务
function scheduleUpdateOnFiber(fiber, lane) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (lane === SyncLane) {
// 同步更新
performSyncWorkOnRoot(root);
} else {
// 并发更新
scheduleCallback(NormalPriority, performConcurrentWorkOnRoot.bind(null, root));
}
}
7. Hooks与Fiber
7.1 Hooks的实现
Hooks依赖于Fiber节点的memoizedState字段:
// Hook对象结构
const hook = {
memoizedState: null, // 上次渲染的状态
baseState: null, // 基础状态
baseQueue: null, // 基础更新队列
queue: null, // 更新队列
next: null, // 下一个Hook
};
7.2 useState的工作流程
function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function mountState(initialState) {
const hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
});
const dispatch = (queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue));
return [hook.memoizedState, dispatch];
}
8. 错误边界
8.1 错误处理机制
Fiber提供了更好的错误边界处理:
function throwException(root, returnFiber, sourceFiber, value, rootRenderLanes) {
sourceFiber.flags |= Incomplete;
// 寻找最近的错误边界
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case ClassComponent:
if (workInProgress.type.getDerivedStateFromError !== undefined ||
workInProgress.stateNode.componentDidCatch !== undefined) {
// 找到错误边界
return throwException(root, workInProgress, sourceFiber, value, rootRenderLanes);
}
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
9. 性能优化
9.1 bailout策略
Fiber通过多种策略来跳过不必要的工作:
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
// 检查子节点是否需要更新
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// 子节点不需要更新,直接跳过
return null;
}
// 克隆子节点继续工作
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
9.2 时间切片
// 检查是否应该让出控制权
function shouldYield() {
return getCurrentTime() >= deadline;
}
// Scheduler中的时间切片实现
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
10. Fiber与并发特性
10.1 Concurrent Mode
Fiber为React的并发模式奠定了基础:
- 可中断渲染:长任务可以被分割成小片段
- 优先级调度:高优先级任务可以打断低优先级任务
- 时间切片:将渲染工作分散到多个帧中
10.2 Suspense
Suspense功能依赖于Fiber的错误边界机制:
function throwSuspense(suspense) {
// 抛出特殊的Promise异常
throw suspense;
}
// 在错误边界中捕获Suspense
if (typeof value === 'object' && value !== null && typeof value.then === 'function') {
// 这是一个Suspense Promise
const suspenseBoundary = findSuspenseBoundary(returnFiber);
if (suspenseBoundary !== null) {
suspenseBoundary.flags |= ShouldCapture;
}
}
11. 总结
React Fiber架构的核心优势:
- 可中断性:解决了长任务阻塞主线程的问题
- 优先级调度:让重要的更新能够优先执行
- 错误边界:提供了更好的错误恢复机制
- 并发渲染:为Future的并发特性奠定了基础
- 更好的用户体验:通过时间切片避免页面卡顿
Fiber架构的引入使React从一个简单的UI库演进为一个强大的用户界面开发平台,为复杂应用的开发提供了强有力的支持。理解Fiber架构对于深入掌握React的工作原理和性能优化具有重要意义。
四、React Hooks
1. useState
用于在函数组件中添加状态管理功能。它让函数组件能够拥有自己的内部状态。
(1)基本语法
const [state, setState] = useState(initialValue);
state: 当前状态值setState: 更新状态的函数initialValue: 状态的初始值
(2)重要特性
- 状态更新是异步的
function AsyncExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('点击前:', count); // 0
setCount(count + 1);
console.log('点击后:', count); // 仍然是 0,因为状态更新是异步的
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>增加</button>
</div>
);
}
- 函数式更新:当新状态依赖于前一个状态时,需使用函数式更新:
function Counter() {
const [count, setCount] = useState(0);
const incrementTwice = () => {
// 错误方式:可能不会按预期工作
// setCount(count + 1);
// setCount(count + 1);
// 正确方式:使用函数式更新
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementTwice}>增加2</button>
</div>
);
}
- React 会对多个状态更新进行批处理:
function BatchingExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// 这些更新会被批处理,只触发一次重新渲染
setCount(c => c + 1);
setFlag(f => !f);
};
console.log('渲染'); // 只会打印一次
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>更新</button>
</div>
);
}
(3)简单实现
// 存放当前组件中所有 hook 的状态值
// 比如 useState(0)、useState('hi') 都会在这里按顺序存储
let hookStates = [];
// 当前正在执行的 hook 的索引(位置)
// 每次执行一个 useState,就会往后移动一位
let hookIndex = 0;
// 模拟 React 的 useState 实现
function useState(initialValue) {
// 记录当前 useState 在 hooks 数组中的位置
const currentIndex = hookIndex;
// 初始化阶段(第一次渲染):
// 如果当前索引没有值,则用 initialValue 初始化
// 否则说明已经渲染过,直接复用旧值
hookStates[currentIndex] = hookStates[currentIndex] ?? initialValue;
// 定义更新函数(相当于 setCount)
const setState = (newValue) => {
// 支持两种形式:
// 1. 直接传值:setCount(2)
// 2. 传函数:setCount(prev => prev + 1)
hookStates[currentIndex] =
typeof newValue === 'function'
? newValue(hookStates[currentIndex]) // 函数式更新
: newValue; // 直接赋值
// 状态更新后重新渲染组件
render();
};
// 每调用一个 useState,索引加 1,保证下一个 Hook 能存到不同位置
hookIndex++;
// 返回当前状态值和更新函数
return [hookStates[currentIndex], setState];
}
// 模拟组件重新渲染
function render() {
// 每次重新渲染前,将索引重置为 0
// 因为组件会重新从头执行(React 每次 render 都是函数重跑)
hookIndex = 0;
// 执行函数组件本身
App();
}
2. useEffect
用于处理副作用,包括:请求数据、设置订阅、操作 DOM、定时器、清理资源。
(1)基本语法
useEffect(() => {
console.log('组件挂载或更新');
return () => {
console.log('组件卸载或清理');
};
}, [count]); // 依赖项改变时才执行
第二个参数是依赖数组: 空数组 []时,只在挂载时执行一次;有依赖项时,依赖项变化时执行;无依赖数组时,每次渲染都执行。
(2)在return 中,可以取消请求、清理定时器、移除事件监听器
// 取消请求
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const abortController = new AbortController();
const searchData = async () => {
try {
setLoading(true);
const response = await fetch(`/api/search?q=${query}`, {
signal: abortController.signal
});
const data = await response.json();
setResults(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
}
} finally {
setLoading(false);
}
};
searchData();
// 清理函数:取消请求
return () => {
abortController.abort();
};
}, [query]);
return (
<div>
{loading && <div>搜索中...</div>}
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
3. useContext
用于在组件树中跨层级传递数据,避免了逐层传递 props的问题。能够在不通过 props 的情况下,将数据传递给深层嵌套的组件。
- Context: 上下文对象,用于存储共享数据
- Provider: 提供者组件,用于提供数据
- Consumer: 消费者组件,用于使用数据
import React, { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const ThemeContext = createContext();
// 2. 创建 Provider 组件
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. 在子组件中使用 Context
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}>
<h1>我的应用</h1>
<button onClick={toggleTheme}>
切换到 {theme === 'light' ? '深色' : '浅色'} 模式
</button>
</header>
);
}
function Content() {
const { theme } = useContext(ThemeContext);
return (
<main style={{
backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px'
}}>
<p>当前主题: {theme}</p>
</main>
);
}
// 4. 在应用中使用
function App() {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
}
4. useRef
用于获取 DOM 节点或保存可变值,useRef 返回一个对象,该对象有一个 current 属性,可以通过修改 current 来存储任何值。与 useState 不同的是,修改 useRef 的值不会触发组件重新渲染。
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
5. useMemo
可以缓存计算结果,只有在依赖项发生变化时才会重新计算。
(1)基本语法
import { useMemo } from 'react';
const memoizedValue = useMemo(() => {
return expensiveCalculation(a, b);
}, [a, b]);
useMemo 接收两个参数:
- 计算函数:返回需要缓存的值
- 依赖数组:当数组中的值发生变化时,才会重新执行计算函数
(2) 深比较函数
function deepEqual(a, b) {
// 1️⃣ 如果两者是同一个引用或基本类型值相等
if (a === b) return true;
// 2️⃣ 处理 NaN(因为 NaN !== NaN,但语义上应视为相等)
if (Number.isNaN(a) && Number.isNaN(b)) return true;
// 3️⃣ 如果类型不同,或其中一个是 null(typeof null === 'object')
if (typeof a !== typeof b || a === null || b === null) return false;
// 4️⃣ 如果是基本类型(非对象、非数组),直接比较
if (typeof a !== "object") return a === b;
// 5️⃣ 处理 Date 对象(用时间戳比较)
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// 6️⃣ 处理正则对象(用源和标志位比较)
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// 7️⃣ 如果是数组
if (Array.isArray(a) && Array.isArray(b)) {
// 长度不同,直接 false
if (a.length !== b.length) return false;
// 逐项递归比较
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
// 8️⃣ 如果是普通对象(非数组)
const keysA = Object.keys(a);
const keysB = Object.keys(b);
// 键数量不同,直接不相等
if (keysA.length !== keysB.length) return false;
// 检查每个 key 的存在性和对应值
for (const key of keysA) {
// b 中没有相同 key,直接 false
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
// 递归比较子属性
if (!deepEqual(a[key], b[key])) return false;
}
// 9️⃣ 所有 key 都通过,返回 true
return true;
}
(3)深比较useMemo
function useDeepMemo(factory, deps) {
// 用于保存上一次的依赖数组
const lastDepsRef = useRef();
// 用于保存上一次计算得到的结果
const lastValueRef = useRef();
// 🧠 如果已经有缓存过依赖(即不是第一次执行)
// 且当前依赖与上次依赖“深比较相等”,说明依赖没有变化
if (lastDepsRef.current && deepEqual(lastDepsRef.current, deps)) {
// ✅ 直接返回上一次计算的结果,避免重复计算
return lastValueRef.current;
}
// 🚀 如果依赖发生变化(或者是第一次执行),重新调用 factory() 计算结果
const newValue = factory();
// 📝 把当前依赖和计算结果缓存起来
// 以便下次渲染时比较依赖变化,决定是否复用结果
lastDepsRef.current = deps;
lastValueRef.current = newValue;
// 返回最新计算结果
return newValue;
}
6. useCallback
用于缓存函数,返回一个记忆化的回调函数,只有在依赖项发生变化时才会重新创建函数。
import { useCallback } from 'react';
const memoizedCallback = useCallback(() => {
// 函数逻辑
}, [dependency1, dependency2]);
useCallback 接收两个参数:
- 回调函数:需要缓存的函数
- 依赖数组:当数组中的值发生变化时,才会重新创建函数
7. useReducer
useState 的替代方案,特别适用于复杂的状态逻辑管理。
import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
function Counter() {
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</div>
);
}