在React开发中,我们经常会写这样的代码:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return (
<button onClick={() => {
setCount(count + 1);
setStep(step + 1);
}}>
当前计数 {count}
当前步长 {step}
</button>
);
}
这个简单的计数器组件看似平常,但它却涉及了React状态管理的核心机制。当我们点击按钮时,React是如何处理这两个状态更新的?它们是立即更新还是批量处理?更新后,组件是如何重新渲染的? 让我们深入React的源码实现,揭开这些问题的答案
hook链表示意图
首先,React需要一种方式来保存组件的多个状态。它使用了一个精心设计的链表结构:
interface Hook {
memorizedState: any; // 保存状态值
updateQueue: unknown; // 更新队列
next: Hook | null; // 指向下一个Hook
}
这个看似简单的结构实际上是React状态管理的核心。让我们来解析它的设计思想:
当组件首次渲染时,每个Hook都会被创建并加入链表,以useState为例:
// 【首次渲染的 useState】
function mountState<State>(initialState: State): [State, Dispatch<State>] {
// 1. 创建新的Hook节点,挂载到Fiber的memorizedState上
// 2. 其余hook依次挂在最后一个hook的next下
const hook = mountWorkInProgressHook();
// 2. 保存初始状态
hook.memorizedState = typeof initialState === 'function'
? initialState()
: initialState;
// 3. 创建更新队列
const queue = createUpdateQueue<State>();
hook.updateQueue = queue;
// 4. 创建dispatch函数
const dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
);
return [hook.memorizedState, dispatch];
}
这些Hook节点会按照调用顺序连接成一个链表,保存在组件对应的Fiber节点上。
当我们调用setCount(count + 1)时,实际上调用的是dispatchSetState:
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
// count + 1 就是action,action也可以是函数
action: Action<State>
) {
// 1. 获取本次更新的优先级
const lane = requestUpdateLane();
// 2. 创建更新对象
const update = createUpdate(action, lane);
// 3. 将更新加入队列
enqueueUpdate(updateQueue, update);
// 4. 调度更新
scheduleUpdateOnFiber(fiber, lane);
}
这个过程不会立即更新状态,而是将更新放入队列中,这就是为什么我们可以在一个事件处理函数中多次调用setState而只会触发一次渲染。
更新链式队列示意图
当React开始处理这些更新时(注意useState的触发时机是FC组件重新被调用,下文提到的 renderWithHooks 会描述调用FC的过程):
// 【首次渲染之后实际调用的 useState】
function updateState<State>(): [State, Dispatch<State>] {
// 1. 会从currentlyRenderingFiber中取alternate(当前页面使用的fiber) 取hook复用
// 2. currentlyRenderingFiber 是全局变量,会在调用Function组件之前被赋值【见renderWithHooks】
const hook = updateWorkInProgressHook();
const queue = hook.updateQueue as UpdateQueue<State>;
// 获取并清空pending队列
const pending = queue.shared.pending;
queue.shared.pending = null;
if (pending !== null) {
// 计算最新的状态
const { memorizedState } = processUpdateQueue(
hook.memorizedState,
pending,
renderLane
);
hook.memorizedState = memorizedState;
}
return [hook.memorizedState, queue.dispatch];
}
React会批量处理队列中的所有更新,计算出最终的状态值。
更新被调度后,React会启动一个完整的渲染流程:
function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
// 找到根节点
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// 请求调度
ensureRootIsScheduled(root);
}
这个过程涉及React的几个核心概念:
从触发更新的组件开始,React会:
1. 构建workInProgress树
export function renderWithHooks(workInProgress: FiberNode, lane: Lane) {
currentlyRenderingFiber = workInProgress;
// 重置Hook链表,准备新的渲染
workInProgress.memorizedState = null;
workInProgress.updateQueue = null;
// 执行组件渲染
const Component = workInProgress.type;
// 这里的 Component 就是react项目中见到的FC 也就是一开始说的 Counter 组件
const children = Component(props);
return children;
}
随着 Component的调用,则前文的 updateState也会被调用,自然会触发processUpdateQueue消费更新链式队列,计算出最新的状态,也就是useState的state值。
2. Diff算法 renderWithHooks
返回的新children会和之前的fiber节点进行diff
这是实际更新DOM的阶段,涉及:
整个过程中,React还实现了许多优化机制:
这些机制共同构成了React的”协调引擎”,使得:
React的状态管理实现展现了几个关键的设计思想:
这些设计不仅解决了状态管理的具体问题,更展示了如何构建一个灵活且可维护的系统。通过深入理解这些实现细节,我们不仅能更好地使用React,也能学习到优秀的系统设计思想。
这就是React状态管理的优雅之处:简单的API背后是深思熟虑的设计,每一个选择都在服务于更好的开发体验和系统可维护性。