切记、切记、切记,diff对比的是ReactElement和old fiber,生成新的fiber节点,不是两个fiber对比
🧑💻 前言
之前的文章中,我们从 首次挂载流程 出发,理解了 React 如何一步步"生造"出一棵 Fiber 树,并创建 DOM 节点。
更新阶段则不同:React 不再从零创建 Fiber ,而是基于现有 Fiber 树,通过 Diff ReactElement 与旧 Fiber 来生成新的 Fiber 节点,并决定哪些 DOM 需要更新、移动或删除。
本篇文章,我们将结合源码,逐步拆解 更新阶段的 Diff 流程,理解 React 如何高效复用 Fiber 节点,同时生成新 Fiber 链表。
一、更新阶段与首次挂载的区别
1.1 核心差异对比
维度 | 首次挂载 | 更新阶段 |
---|---|---|
current 树 | 为 null(除 HostRootFiber) | 存在完整的树 |
alternate 指针 | null | 互相指向 |
beginWork 逻辑 | 直接创建子 Fiber | 对比 props,可能复用或更新 |
reconcileChildren | mountChildFibers(不标记) | reconcileChildFibers(执行 diff) |
副作用标记 | 只标记根节点 Placement | 标记 Update、Placement、Deletion |
DOM 操作 | 创建所有 DOM | 复用、更新或删除 DOM |
1.2 双缓冲树在更新时的状态
javascript
// 更新前:
FiberRoot
↓ current
current 树(屏幕显示)
HostRootFiber
↓
AppFiber {
memoizedProps: {count: 0},
memoizedState: {count: 0},
child: divFiber
}
↓
divFiber {
type: 'div',
children: [...]
}
// 更新开始:触发 setState({count: 1})
FiberRoot
↓ current
current 树 workInProgress 树
HostRootFiber ←→ HostRootFiber'
↓ ↓
AppFiber ←→ AppFiber'
{count: 0} {count: 1} (新state)
↓
divFiber
// 通过 alternate 互相指向
// workInProgress 树复用 current 树的节点
// 更新完成:
FiberRoot
↓ current (切换!)
workInProgress 树变成新的 current 树
1.3 更新的三种情况
javascript
// 1. props 没变,state 没变
// 结果:bailout 优化,跳过整个子树
// 2. props 变了,或 state 变了
// 结果:重新 render,执行 diff
// 3. context 变了
// 结果:强制更新
二、更新的触发方式
2.1 类组件的 setState
javascript
class App extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return <div onClick={this.handleClick}>{this.state.count}</div>;
}
}
// setState 的完整流程:
1. 调用 this.setState({count: 1})
↓
2. 创建 update 对象
update = {
payload: {count: 1},
next: null,
}
↓
3. 加入 updateQueue
fiber.updateQueue.shared.pending = update
↓
4. 调度更新
scheduleUpdateOnFiber(fiber, lane)
↓
5. 进入 render 阶段
workLoopSync() 或 workLoopConcurrent()
↓
6. beginWork 处理 AppFiber
↓
7. 计算新 state
processUpdateQueue() → newState = {count: 1}
↓
8. 调用 render()
nextChildren = instance.render()
↓
9. 执行 diff
reconcileChildFibers(current.child, nextChildren)
2.2 函数组件的 useState
javascript
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <div onClick={handleClick}>{count}</div>;
}
// useState 更新流程:
1. 调用 setCount(1)
↓
2. dispatchSetState(fiber, queue, 1)
↓
3. 创建 update 对象
update = {
action: 1,
next: null,
}
↓
4. 加入 hook.queue.pending
queue.pending = update
↓
5. 调度更新
scheduleUpdateOnFiber(fiber, lane)
↓
6. 进入 render 阶段
↓
7. renderWithHooks
- 设置 HooksDispatcherOnUpdate
- 调用 App()
↓
8. 执行 useState(0)
实际调用 updateState()
↓
9. 计算新 state
遍历 queue.pending,计算最终值 = 1
↓
10. 返回 [1, setCount]
↓
11. render() 返回新的 children
↓
12. 执行 diff
2.3 forceUpdate 强制更新
javascript
class App extends React.Component {
handleClick = () => {
this.forceUpdate(); // 跳过 shouldComponentUpdate
};
render() {
return <div onClick={this.handleClick}>Force Update</div>;
}
}
// forceUpdate 的特点:
// 1. 不检查 props 和 state
// 2. 强制执行 render
// 3. 子组件仍然会进行 diff
三、beginWork 更新路径
3.1 beginWork 的更新判断
javascript
// 📍 位置:ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
// ========== 更新路径 ==========
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 步骤1:检查 props 是否变化
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// Props 变了,标记需要更新
didReceiveUpdate = true;
} else {
// Props 没变,检查其他更新来源
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
// 🎯 完全不需要更新,执行 bailout
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
// 首次挂载路径
didReceiveUpdate = false;
}
// 清空 lanes
workInProgress.lanes = NoLanes;
// 根据 tag 分发
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes
);
}
// 其他类型...
}
}
3.2 bailout 优化机制
javascript
function attemptEarlyBailoutIfNoScheduledUpdate(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
/* 💡 bailout 的条件:
* 1. props 没变(oldProps === newProps)
* 2. 没有 state 更新(没有 setState)
* 3. context 没变
* 4. 没有 forceUpdate
*/
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case HostRoot:
// 根节点特殊处理
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
// 其他类型...
}
// 🔥 关键:直接克隆子节点,不执行 render
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
// 复用 current 的依赖
workInProgress.dependencies = current.dependencies;
}
// 检查子节点是否有更新
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// 🎯 子树也没有更新,跳过整个子树!
return null;
}
// 子树有更新,克隆子节点
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
bailout 示例:
jsx
class Parent extends React.Component {
state = { count: 0 };
render() {
return (
<div>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
<Child name="React" /> {/* props 没变 */}
</div>
);
}
}
class Child extends React.Component {
render() {
console.log("Child render"); // 不会执行!
return <div>{this.props.name}</div>;
}
}
// 点击 button 触发更新:
// 1. Parent 重新 render
// 2. Child 的 props 没变(name="React")
// 3. Child bailout,不执行 render
// 4. 直接复用 Child 的子树
四、Diff 算法核心原理
4.1 传统 Diff 的问题
scss
传统的树 diff 算法:
- 时间复杂度:O(n³)
- 对于 1000 个节点,需要 10 亿次比较
- 完全不可用
为什么是 O(n³)?
1. 遍历树1的每个节点:O(n)
2. 遍历树2的每个节点:O(n)
3. 计算最小编辑距离:O(n)
总共:O(n³)
4.2 React Diff 的三个策略
React 通过三个假设,将复杂度降到 O(n):
javascript
// 策略1:Tree Diff - 只比较同层级
/* 💡 假设:
* DOM 节点跨层级移动的情况非常少见
*
* 结果:
* - 只比较同一层级的节点
* - 不同层级的节点不比较
* - 如果节点跨层级移动,会删除重建
*/
旧树: 新树:
A A
↙ ↘ ↙ ↘
B C D C
↙ ↙
D B
// React 的处理:
// 1. A 层:A 保留
// 2. B、C 层:
// - B 删除
// - C 保留
// - D 创建(在 C 下)
// 3. D 层:
// - D 下的 B 创建(新的 B)
// 不会识别 B 移动到了 D 下!
// 策略2:Component Diff - 同类型组件对比
/* 💡 假设:
* - 同类型组件生成相似的树结构
* - 不同类型组件生成不同的树结构
*
* 结果:
* - 同类型组件:继续对比子节点(Virtual DOM Diff)
* - 不同类型组件:直接删除重建
*/
// 例子1:同类型组件
<div className="old"> → <div className="new">
<p>text</p> <p>text</p>
</div> </div>
// 处理:
// 1. div 类型相同,复用 Fiber
// 2. 更新 className: old → new
// 3. 继续对比子节点 p
// 例子2:不同类型组件
<div> → <span>
<p>text</p> <p>text</p>
</div> </span>
// 处理:
// 1. div → span,类型不同
// 2. 删除整个 div 子树(包括 p)
// 3. 创建整个 span 子树(包括 p)
// 4. 不会复用 p!
// 策略3:Element Diff - 同层级节点对比
/* 💡 假设:
* - 通过 key 可以准确识别节点
* - 有 key 的节点可以跨位置复用
*
* 结果:
* - 有 key:通过 key 匹配,可以移动
* - 无 key:通过 index 匹配,按顺序对比
*/
// 例子:列表更新
旧:[A, B, C, D]
新:[D, A, B, C]
// 有 key 的情况:
// 1. 通过 key 识别 D 移动到了最前面
// 2. A、B、C 位置变化,但复用 Fiber
// 3. 只需要移动 DOM 节点
// 无 key 的情况:
// 1. 按 index 对比:
// - 0: A → D (更新)
// - 1: B → A (更新)
// - 2: C → B (更新)
// - 3: D → C (更新)
// 2. 所有节点都需要更新!
4.3 reconcileChildren 的分发逻辑
javascript
// 📍 位置:ReactChildFiber.new.js
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 首次挂载
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 🔥 更新:执行 diff
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
// reconcileChildFibers 的内部实现
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
// 判断 newChild 的类型,分发到不同的处理函数
// 1. 单个元素
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 🔥 单节点 diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)
);
}
// 2. 数组(多个子节点)
if (isArray(newChild)) {
// 🔥 多节点 diff
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
}
// 3. 文本节点
if (typeof newChild === "string" || typeof newChild === "number") {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
lanes
)
);
}
// 4. Fragment
if (typeof newChild === "object" && newChild !== null) {
if (newChild.$$typeof === REACT_LAZY_TYPE) {
// lazy 组件
}
}
// 5. 删除剩余节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
五、单节点 Diff
5.1 单节点 Diff 的流程
javascript
// 📍 位置:ReactChildFiber.new.js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
// ========== 第一步:遍历所有旧子节点,尝试复用 ==========
while (child !== null) {
// 1.1 比较 key
if (child.key === key) {
// key 相同,继续比较 type
const elementType = element.type;
// 1.2 比较 type
if (child.elementType === elementType ||
/* 其他类型判断 */) {
// 🎯 key 和 type 都相同,可以复用!
// 删除剩余的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用 current Fiber,创建 workInProgress Fiber
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
} else {
// key 相同但 type 不同,删除所有旧节点
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// key 不同,删除这个节点,继续找
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// ========== 第二步:没有找到可复用的,创建新节点 ==========
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
5.2 单节点 Diff 的四种情况
javascript
// 情况1:key 和 type 都相同 - 复用
// 旧:<div key="a">old</div>
// 新:<div key="a">new</div>
reconcileSingleElement() {
// child.key === "a",element.key === "a" ✓
// child.type === "div",element.type === "div" ✓
// 结果:复用 Fiber,更新 props
}
Fiber 变化:
div Fiber {
key: "a",
type: "div",
memoizedProps: {children: "old"}, // 旧 props
pendingProps: {children: "new"}, // 新 props
stateNode: <div>old</div>, // 复用 DOM
flags: Update, // 标记更新
}
// 情况2:key 相同但 type 不同 - 删除旧的,创建新的
// 旧:<div key="a">text</div>
// 新:<span key="a">text</span>
reconcileSingleElement() {
// child.key === "a",element.key === "a" ✓
// child.type === "div",element.type === "span" ✗
// 结果:删除 div Fiber,创建 span Fiber
}
操作:
1. divFiber.flags |= Deletion // 标记删除
2. 创建 spanFiber
3. spanFiber.flags |= Placement // 标记插入
// 情况3:key 不同 - 继续找或创建新的
// 旧:<div key="a">text</div><p key="b">text</p>
// 新:<div key="b">text</div>
reconcileSingleElement() {
// 第一次循环:
// child.key === "a",element.key === "b" ✗
// deleteChild(divFiber)
// 第二次循环:
// child.key === "b",element.key === "b" ✓
// child.type === "p",element.type === "div" ✗
// deleteRemainingChildren(pFiber)
// 创建新的 divFiber
}
操作:
1. divFiber(key="a").flags |= Deletion
2. pFiber(key="b").flags |= Deletion
3. 创建新的 divFiber(key="b")
4. 新 divFiber.flags |= Placement
// 情况4:旧节点为 null - 直接创建
// 旧:null
// 新:<div key="a">text</div>
reconcileSingleElement() {
// child === null
// 跳过 while 循环
// 创建新 Fiber
}
5.3 useFiber - 复用 Fiber 的关键
javascript
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
// 复用 current Fiber,创建 workInProgress Fiber
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 首次更新,创建 alternate
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode; // 🔥 复用 DOM!
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 再次更新,复用 alternate
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
// 重置副作用
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
// 复用其他属性
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// ... 复用更多属性
return workInProgress;
}
六、多节点 Diff
6.1 多节点 Diff 的挑战
javascript
// 多节点 diff 需要处理的情况:
// 1. 节点更新
旧:[<div key="a">old</div>, <div key="b">old</div>]
新:[<div key="a">new</div>, <div key="b">new</div>]
// props 变了,需要更新
// 2. 节点增删
旧:[<div key="a" />, <div key="b" />]
新:[<div key="a" />, <div key="b" />, <div key="c" />]
// 新增了 c
// 3. 节点移动
旧:[<div key="a" />, <div key="b" />, <div key="c" />]
新:[<div key="c" />, <div key="a" />, <div key="b" />]
// c 移动到了最前面
// 4. 混合情况
旧:[<div key="a" />, <div key="b" />, <div key="c" />]
新:[<div key="d" />, <div key="a" />, <div key="e" />]
// b 删除,c 删除,d 新增,e 新增,a 移动
6.2 多节点 Diff 的完整算法
javascript
// 📍 位置:ReactChildFiber.new.js
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes
): Fiber | null {
/* 💡 Diff 算法的两轮遍历:
*
* 第一轮遍历:
* - 处理节点更新的情况
* - 按位置一一对比
* - 遇到不能复用的节点就跳出
*
* 第二轮遍历:
* - 处理节点新增、删除、移动
* - 使用 Map 优化查找
* - 判断是否需要移动
*/
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0; // 最后一个可复用节点的位置
let newIdx = 0;
let nextOldFiber = null;
// ========== 第一轮遍历:处理更新 ==========
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 1. 检查位置是否匹配
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 2. 尝试更新节点
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);
/* 💡 updateSlot 的逻辑:
* - 比较 key
* - key 相同:尝试复用(比较 type)
* - key 不同:返回 null(跳出第一轮)
*/
if (newFiber === null) {
// key 不同,无法复用,跳出第一轮
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 3. 处理不能复用的情况
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 新 Fiber 没有复用 old Fiber,需要删除 old
deleteChild(returnFiber, oldFiber);
}
}
// 4. 记录位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 5. 构建新的 Fiber 链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// ========== 第一轮遍历的三种结束情况 ==========
// 情况1:新节点遍历完,删除剩余旧节点
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 情况2:旧节点遍历完,新增剩余新节点
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 情况3:新旧节点都没遍历完,进入第二轮
// ========== 第二轮遍历:处理移动 ==========
// 1. 将剩余旧节点放入 Map(key → Fiber)
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
/* 💡 Map 结构:
* Map {
* "a" => aFiber,
* "b" => bFiber,
* "c" => cFiber,
* }
*/
// 2. 遍历剩余新节点
for (; newIdx < newChildren.length; newIdx++) {
// 2.1 尝试从 Map 中找到可复用的节点
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
/* 💡 updateFromMap 的逻辑:
* - 通过 key 或 index 在 Map 中查找
* - 找到:尝试复用
* - 找不到:创建新节点
*/
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 从 Map 中删除已复用的节点
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
// 2.2 判断是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 2.3 构建链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 3. 删除 Map 中剩余的节点(没被复用的)
if (shouldTrackSideEffects) {
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
6.3 placeChild - 判断是否需要移动
javascript
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// 首次渲染,不需要标记
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
// 🔥 节点复用的情况
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 🔥 需要移动!
/* 💡 为什么?
* lastPlacedIndex 记录的是"最后一个不需要移动的节点"的位置
*
* 如果 oldIndex < lastPlacedIndex,说明:
* - 这个节点在旧列表中的位置,比"最后一个不需要移动的节点"还靠前
* - 但现在它在新列表中的位置,比"最后一个不需要移动的节点"还靠后
* - 所以需要向右移动
*/
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// 不需要移动
return oldIndex;
}
} else {
// 新节点
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
6.4 多节点 Diff 完整示例
javascript
// 示例:理解 lastPlacedIndex
旧列表:A(0) - B(1) - C(2) - D(3)
新列表:D - A - B - C
// 第一轮遍历:
newIdx=0, 新节点D, 旧节点A
- D.key !== A.key
- 跳出第一轮
// 第二轮遍历:
// 构建 Map:{A: AFiber, B: BFiber, C: CFiber, D: DFiber}
// lastPlacedIndex = 0
newIdx=0, 新节点D:
- 从Map找到DFiber, oldIndex=3
- oldIndex(3) >= lastPlacedIndex(0) ✓
- 不需要移动
- lastPlacedIndex = 3
- 标记:DFiber.flags = 0
newIdx=1, 新节点A:
- 从Map找到AFiber, oldIndex=0
- oldIndex(0) < lastPlacedIndex(3) ✗
- 🔥 需要移动!
- lastPlacedIndex = 3 (不变)
- 标记:AFiber.flags |= Placement
newIdx=2, 新节点B:
- 从Map找到BFiber, oldIndex=1
- oldIndex(1) < lastPlacedIndex(3) ✗
- 🔥 需要移动!
- lastPlacedIndex = 3 (不变)
- 标记:BFiber.flags |= Placement
newIdx=3, 新节点C:
- 从Map找到CFiber, oldIndex=2
- oldIndex(2) < lastPlacedIndex(3) ✗
- 🔥 需要移动!
- lastPlacedIndex = 3 (不变)
- 标记:CFiber.flags |= Placement
结果:
- D 不动(作为参考点)
- A、B、C 都标记 Placement(移动到 D 后面)
实际 DOM 操作:
parent.insertBefore(A, D.nextSibling)
parent.insertBefore(B, A.nextSibling)
parent.insertBefore(C, B.nextSibling)
优化的例子:
javascript
// 反例:最差情况
旧列表:A(0) - B(1) - C(2) - D(3)
新列表:D - C - B - A
// Diff 结果:
// lastPlacedIndex 会被 D(3) 设置为 3
// A、B、C 的 oldIndex 都 < 3
// 所以 A、B、C 都需要移动
// 更好的方案:只移动 D
// 但 React 的算法不会识别这种情况
// 因为算法是从左到右遍历的
// 最佳实践:把不变的节点放在前面
旧列表:A(0) - B(1) - C(2) - D(3)
新列表:A - B - C - D - E
// Diff 结果:
// A、B、C、D 都不需要移动
// 只有 E 需要插入
七、Key 的作用机制
7.1 为什么需要 key?
jsx
// 场景:列表头部插入
// 没有 key:
旧:[<li>1</li>, <li>2</li>, <li>3</li>]
新:[<li>0</li>, <li>1</li>, <li>2</li>, <li>3</li>]
// Diff 过程(按 index 对比):
index=0: <li>1</li> → <li>0</li> // 更新文本
index=1: <li>2</li> → <li>1</li> // 更新文本
index=2: <li>3</li> → <li>2</li> // 更新文本
index=3: null → <li>3</li> // 插入
// 结果:4次操作(3次更新 + 1次插入)
// 有 key:
旧:[<li key="1">1</li>, <li key="2">2</li>, <li key="3">3</li>]
新:[<li key="0">0</li>, <li key="1">1</li>, <li key="2">2</li>, <li key="3">3</li>]
// Diff 过程(通过 key 对比):
key="0": 新节点,插入
key="1": 找到,不需要移动
key="2": 找到,不需要移动
key="3": 找到,不需要移动
// 结果:1次操作(1次插入)
7.2 key 的匹配逻辑
javascript
// 在 updateSlot 中使用 key
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 🔥 比较 key
if (newChild.key === key) {
// key 相同,尝试复用
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
// key 不同,返回 null
return null;
}
}
}
}
return null;
}
// 在 updateFromMap 中使用 key
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
lanes: Lanes
): Fiber | null {
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 🔥 通过 key 或 index 查找
const matchedFiber =
existingChildren.get(newChild.key === null ? newIdx : newChild.key) ||
null;
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
}
}
return null;
}
7.3 key 的最佳实践
jsx
// ❌ 错误1:使用 index 作为 key
{
list.map((item, index) => <Item key={index} data={item} />);
}
// 问题:
// 当列表顺序变化时,index 不会变
// 但对应的 item 变了
// 导致错误的复用
// ❌ 错误2:使用随机数作为 key
{
list.map((item) => <Item key={Math.random()} data={item} />);
}
// 问题:
// 每次 render,key 都会变
// 永远无法复用
// 等于没有 key
// ❌ 错误3:key 不稳定
{
list.map((item) => <Item key={item.id + Math.random()} data={item} />);
}
// 问题:
// key 在不同 render 之间会变化
// 无法复用
// ✅ 正确:使用稳定的唯一标识
{
list.map((item) => <Item key={item.id} data={item} />);
}
// 要求:
// 1. 唯一:在兄弟节点中唯一
// 2. 稳定:不会在 render 之间变化
// 3. 可预测:同一个 item 总是有同一个 key
// ✅ 如果没有 id,可以使用其他唯一标识
{
list.map((item) => <Item key={item.name + item.type} data={item} />);
}
// ✅ 只有在列表是静态的(不会重排)时,才能用 index
const STATIC_LIST = ["Home", "About", "Contact"];
{
STATIC_LIST.map((item, index) => (
<Link key={index} to={item}>
{item}
</Link>
));
}
7.4 key 的底层存储
javascript
// key 在 Fiber 节点中的存储
FiberNode {
key: string | null, // 🔥 key 值
// 通过 key 可以快速匹配节点
}
// 在 Map 中使用 key
const map = new Map();
// 有 key 的节点
map.set(fiber.key, fiber); // "a" → aFiber
// 没有 key 的节点
map.set(fiber.index, fiber); // 0 → fiber
// 查找节点
const key = element.key;
const matchedFiber = map.get(key !== null ? key : index);
八、完整更新流程实例
8.1 示例代码
jsx
class App extends React.Component {
state = {
list: [
{ id: "a", text: "A" },
{ id: "b", text: "B" },
{ id: "c", text: "C" },
],
};
handleClick = () => {
// 更新:移动 + 删除 + 新增
this.setState({
list: [
{ id: "c", text: "C-updated" }, // 移动到最前面,且文本变化
{ id: "a", text: "A" }, // 移动
{ id: "d", text: "D" }, // 新增
],
});
};
render() {
return (
<div>
<button onClick={this.handleClick}>Update</button>
<ul>
{this.state.list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
);
}
}
8.2 完整的 Diff 执行过程
ini
=== 更新前的 Fiber 树 ===
AppFiber {
memoizedState: {
list: [
{id: 'a', text: 'A'},
{id: 'b', text: 'B'},
{id: 'c', text: 'C'},
]
}
}
↓
divFiber
↓
ulFiber
↓ child
liFiber(key="a") → liFiber(key="b") → liFiber(key="c")
↓ ↓ sibling ↓ sibling
textFiber("A") textFiber("B") textFiber("C")
=== 点击按钮,触发 setState ===
1. 创建 update 对象
2. 放入 updateQueue
3. 调度更新
4. 进入 Render 阶段
=== beginWork(AppFiber) ===
1. current !== null,进入更新路径
2. oldProps === newProps,但有 state 更新
3. didReceiveUpdate = true
4. processUpdateQueue()
- 计算新 state:
{
list: [
{id: 'c', text: 'C-updated'},
{id: 'a', text: 'A'},
{id: 'd', text: 'D'},
]
}
5. instance.render()
- 返回新的 children
=== reconcileChildren(ulFiber, newChildren) ===
旧子节点:
currentFirstChild = liFiber(key="a")
↓ sibling
liFiber(key="b")
↓ sibling
liFiber(key="c")
新子节点(数组):
newChildren = [
<li key="c">C-updated</li>,
<li key="a">A</li>,
<li key="d">D</li>,
]
进入 reconcileChildrenArray()
--- 第一轮遍历 ---
newIdx=0:
- 新节点:<li key="c">
- 旧节点:liFiber(key="a")
- updateSlot():
- newChild.key="c", oldFiber.key="a"
- key 不同,返回 null
- 跳出第一轮遍历
--- 第二轮遍历 ---
// 构建 Map
existingChildren = Map {
"a" => liFiber(key="a", index=0),
"b" => liFiber(key="b", index=1),
"c" => liFiber(key="c", index=2),
}
lastPlacedIndex = 0
newIdx=0, 新节点<li key="c">C-updated</li>:
- updateFromMap():
- 从 Map 中找到 liFiber(key="c")
- oldIndex = 2
- 比较 type:"li" === "li" ✓
- 复用 Fiber,更新 props
- placeChild():
- oldIndex(2) >= lastPlacedIndex(0) ✓
- 不需要移动
- lastPlacedIndex = 2
- 从 Map 删除:
existingChildren.delete("c")
- 结果:
newCFiber = useFiber(oldCFiber, {children: "C-updated"})
newCFiber.flags = Update // props 变化
newIdx=1, 新节点<li key="a">A</li>:
- updateFromMap():
- 从 Map 中找到 liFiber(key="a")
- oldIndex = 0
- 复用 Fiber
- placeChild():
- oldIndex(0) < lastPlacedIndex(2) ✗
- 🔥 需要移动!
- lastPlacedIndex = 2 (不变)
- 从 Map 删除:
existingChildren.delete("a")
- 结果:
newAFiber = useFiber(oldAFiber, {children: "A"})
newAFiber.flags = Placement // 移动
newIdx=2, 新节点<li key="d">D</li>:
- updateFromMap():
- 从 Map 中找不到 key="d"
- 创建新 Fiber
- placeChild():
- current === null
- 新节点
- lastPlacedIndex = 2 (不变)
- 结果:
newDFiber = createFiber({children: "D"})
newDFiber.flags = Placement // 插入
遍历结束,Map 中剩余:
existingChildren = Map {
"b" => liFiber(key="b"),
}
删除剩余节点:
- liFiber(key="b").flags |= Deletion
--- 第二轮遍历结果 ---
新的 Fiber 链表:
ulFiber.child = newCFiber
↓ sibling
newAFiber
↓ sibling
newDFiber
副作用标记:
- newCFiber.flags = Update
- newAFiber.flags = Placement
- newDFiber.flags = Placement
- oldBFiber.flags = Deletion
deletions 数组:
ulFiber.deletions = [oldBFiber]
=== completeWork ===
// 从下往上完成
// DOM 在 Commit 阶段处理
=== Commit 阶段(简要)===
1. 删除阶段:
- ul.removeChild(oldBFiber.stateNode) // 删除 B
2. 更新阶段:
- newCFiber.stateNode.textContent = "C-updated" // 更新 C 的文本
3. 插入阶段:
- ul.insertBefore(newAFiber.stateNode, referenceNode) // 移动 A
- ul.appendChild(newDFiber.stateNode) // 插入 D
最终 DOM:
<ul>
<li>C-updated</li> <!-- 复用,更新 -->
<li>A</li> <!-- 复用,移动 -->
<li>D</li> <!-- 新建,插入 -->
</ul>
8.3 副作用的收集和执行
javascript
// Render 阶段收集的副作用:
ulFiber {
flags: NoFlags,
subtreeFlags: Update | Placement | Deletion,
deletions: [oldBFiber],
child: newCFiber,
}
newCFiber {
flags: Update, // 文本从 "C" → "C-updated"
sibling: newAFiber,
}
newAFiber {
flags: Placement, // 需要移动
sibling: newDFiber,
}
newDFiber {
flags: Placement, // 需要插入
sibling: null,
}
oldBFiber {
flags: Deletion, // 需要删除
}
// Commit 阶段的执行顺序:
// 1. Mutation 阶段(DOM 操作)
commitMutationEffects() {
// 1.1 删除
commitDeletionEffects(ulFiber.deletions)
- ul.removeChild(oldBFiber.stateNode)
// 1.2 更新
commitWork(newCFiber)
- updateDOMProperties(newCFiber.stateNode, oldProps, newProps)
- li.textContent = "C-updated"
// 1.3 插入
commitPlacement(newAFiber)
- const before = getHostSibling(newAFiber) // 找参考节点
- ul.insertBefore(newAFiber.stateNode, before)
commitPlacement(newDFiber)
- ul.appendChild(newDFiber.stateNode)
}
// 2. Layout 阶段(生命周期)
commitLayoutEffects() {
// 调用 componentDidUpdate、useLayoutEffect 等
}
九、常见问题深度解答
Q1: 为什么 React 不采用双端 Diff?
深度解答:
Vue 使用双端 Diff(从两端向中间遍历),而 React 使用单端 Diff(从左向右遍历)。
javascript
// Vue 的双端 Diff(伪代码)
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartIdx.key === newStartIdx.key) {
// 头头比较
} else if (oldEndIdx.key === newEndIdx.key) {
// 尾尾比较
} else if (oldStartIdx.key === newEndIdx.key) {
// 头尾比较
} else if (oldEndIdx.key === newStartIdx.key) {
// 尾头比较
} else {
// 使用 key 查找
}
}
// React 为什么不用?
// 1. React 的 Fiber 架构不适合双端遍历
// 2. Fiber 链表只有 child 和 sibling,没有反向指针
// 3. 添加反向指针会增加内存开销
// 4. React 的时间切片机制需要可随时中断,双端遍历不易实现
// 5. 实际场景中,从左到右遍历已经足够高效
Q2: 为什么多节点 Diff 需要两轮遍历?
深度解答:
javascript
// 原因1:处理最常见的情况 - 节点更新
// 大部分更新场景:只是 props 变化,顺序不变
旧:[A, B, C, D]
新:[A', B', C', D'] // 只是内容变化
// 第一轮遍历可以快速处理:
for (let i = 0; i < newChildren.length; i++) {
if (canReuse(oldChildren[i], newChildren[i])) {
update(oldChildren[i], newChildren[i]);
} else {
break; // 遇到不能复用的,跳出
}
}
// 如果第一轮就完成了,不需要第二轮
// 避免创建 Map 的开销
// 原因2:Map 的创建有成本
// 只有在需要查找时才创建 Map
// 第一轮遍历发现需要查找,才进入第二轮
const map = new Map(); // 创建 Map 有成本
for (let fiber of oldChildren) {
map.set(fiber.key, fiber);
}
Q3: lastPlacedIndex 的原理是什么?
深度解答:
javascript
// lastPlacedIndex 的含义:
// "最后一个不需要移动的节点"在旧列表中的位置
// 例子1:理解"不需要移动"
旧:A(0) - B(1) - C(2) - D(3)
新:A - B - D - C
// Diff 过程:
A: oldIndex=0, lastPlacedIndex=0, 0>=0 ✓, 不移动, lastPlacedIndex=0
B: oldIndex=1, lastPlacedIndex=0, 1>=0 ✓, 不移动, lastPlacedIndex=1
D: oldIndex=3, lastPlacedIndex=1, 3>=1 ✓, 不移动, lastPlacedIndex=3
C: oldIndex=2, lastPlacedIndex=3, 2<3 ✗, 移动!
// 为什么 C 需要移动?
// - C 在旧列表中的位置(2),比 D 在旧列表中的位置(3) 靠前
// - 但在新列表中,C 在 D 后面
// - 说明 C 相对于 D 向右移动了
// 例子2:理解算法的局限
旧:A(0) - B(1) - C(2)
新:C - A - B
// Diff 过程:
C: oldIndex=2, lastPlacedIndex=0, 2>=0 ✓, 不移动, lastPlacedIndex=2
A: oldIndex=0, lastPlacedIndex=2, 0<2 ✗, 移动!
B: oldIndex=1, lastPlacedIndex=2, 1<2 ✗, 移动!
// 实际上,只移动 C 到最前面更高效
// 但算法会移动 A 和 B
// 因为算法是从左到右的,C 先被处理,成为"参考点"
// 优化建议:
// 把不需要移动的节点放在前面
旧:A - B - C - D
新:A - B - D - E // C删除,E新增
// 这样只需要删除 C,插入 E
Q4: 什么时候会触发整个子树重建?
深度解答:
jsx
// 情况1:组件类型变化
// 旧:
<div>
<ComponentA />
</div>
// 新:
<div>
<ComponentB />
</div>
// 处理:
// 1. ComponentA 的整个子树被删除
// 2. ComponentB 的整个子树被重建
// 3. 不会尝试复用任何子节点
// 情况2:父节点类型变化
// 旧:
<div>
<span>A</span>
<span>B</span>
</div>
// 新:
<section>
<span>A</span>
<span>B</span>
</section>
// 处理:
// 1. div 及其所有子节点被删除(包括两个 span)
// 2. section 及其所有子节点被重建(包括两个 span)
// 3. 即使 span 的 type 和 key 都相同,也不会复用!
// 情况3:key 变化
// 旧:
<div>
<Item key="a" />
<Item key="b" />
</div>
// 新:
<div>
<Item key="c" />
<Item key="d" />
</div>
// 处理:
// 1. Item(key="a") 被删除
// 2. Item(key="b") 被删除
// 3. Item(key="c") 被创建
// 4. Item(key="d") 被创建
// 避免重建的方法:
// 1. 保持组件类型稳定
// 2. 保持 key 稳定
// 3. 避免条件渲染改变组件类型:
// ❌ 会导致重建
{condition ? <ComponentA /> : <ComponentB />}
// ✅ 在组件内部处理
<Component type={condition ? 'A' : 'B'} />
Q5: Diff 算法的时间复杂度真的是 O(n) 吗?
深度解答:
javascript
// 理论上的时间复杂度:
// 第一轮遍历:O(n)
// - 遍历新子节点数组:n 次
// - 每次对比:O(1)
// 总计:O(n)
// 第二轮遍历:
// - 创建 Map:O(m),m 是剩余旧节点数
// - 遍历剩余新节点:O(k),k 是剩余新节点数
// - 每次从 Map 查找:O(1)
// 总计:O(m + k)
// 总的时间复杂度:O(n + m + k)
// 由于 m + k <= n,所以是 O(n)
// 实际场景的分析:
// 最好情况:只有更新,没有移动
旧:[A, B, C, D]
新:[A', B', C', D']
// 第一轮遍历就完成了,O(n)
// 最坏情况:所有节点都移动
旧:[A, B, C, D]
新:[D, C, B, A]
// 需要两轮遍历,但总体还是 O(n)
// 极端最坏情况:列表非常长,且完全反序
旧:[1, 2, 3, ..., 100000]
新:[100000, ..., 3, 2, 1]
// 仍然是 O(n),但常数因子较大
// 因为每个节点都需要标记 Placement
// 优化:避免最坏情况
// ❌ 完全反序:性能差
list.reverse()
// ✅ 排序:使用稳定排序,尽量保持原有顺序
list.sort((a, b) => a.priority - b.priority)
十、总结
10.1 Diff 算法的核心要点
三个策略:
- Tree Diff:只比较同层级,跨层级移动会重建
- Component Diff:同类型继续比较,不同类型直接重建
- Element Diff:通过 key 识别节点,支持移动和复用
两轮遍历:
- 第一轮:处理更新,按位置对比,遇到不能复用就跳出
- 第二轮:处理移动、新增、删除,使用 Map 优化查找
关键机制:
- key:唯一标识节点,实现精确复用
- lastPlacedIndex:判断节点是否需要移动
- flags:标记副作用(Placement、Update、Deletion)
10.2 更新阶段的完整流程
sql
触发更新(setState/useState/forceUpdate)
↓
创建 update 对象
↓
放入 updateQueue/hook.queue
↓
调度更新 scheduleUpdateOnFiber
↓
进入 Render 阶段
↓
beginWork(更新路径)
├─ current !== null
├─ 比较 props
├─ 检查 updateQueue
└─ 决定 render 或 bailout
↓
render 获取新 children
↓
reconcileChildren(执行 Diff)
├─ 单节点:reconcileSingleElement
└─ 多节点:reconcileChildrenArray
├─ 第一轮:处理更新
└─ 第二轮:处理移动/增删
↓
标记副作用 flags
↓
继续遍历子树
↓
completeWork
↓
收集副作用到父节点
↓
Render 阶段完成
↓
进入 Commit 阶段(执行 DOM 操作)