Appearance
第16章 状态管理库的内核机制
本章要点
- Context 的性能瓶颈根源:Provider value 变化时的全子树重渲染问题与 changedBits 的废弃历史
- useSyncExternalStore 的设计动机:外部状态如何安全接入并发渲染
- Redux Toolkit 的中间件链:compose 与 applyMiddleware 的函数式编程范式
- Zustand 的极简内核:用 200 行代码实现一个完备的状态管理库
- Jotai 的原子依赖图:自底向上的响应式状态传播
- 选型决策框架:从项目规模、团队认知、性能需求三个维度做出理性选择
React 内置了两种状态管理原语:组件内部的 useState/useReducer 和跨组件的 Context。对于中小型应用,这两者足以应对大部分场景。但当应用规模膨胀到一定程度,Context 的性能缺陷和心智负担开始显现——它不是一个真正的状态管理方案,而是一个依赖注入机制。
这就是第三方状态管理库存在的根本原因。Redux、Zustand、Jotai——这些库的诞生不是因为 React 的能力不足,而是因为它们各自找到了不同维度上的最优解。Redux 选择了可预测性,Zustand 选择了极简性,Jotai 选择了细粒度响应性。理解这些库的内核实现,不仅能帮助你做出更合理的技术选型,更能让你理解"状态管理"这个看似简单的问题背后蕴含的深层工程权衡。
本章将从 React 自身的 Context 性能问题出发,深入到 useSyncExternalStore 这个连接 React 并发渲染与外部状态的关键 Hook,然后逐一剖析三大主流状态管理库的内核实现。在这个过程中,你会发现一个有趣的事实:最好的状态管理库往往不是功能最丰富的,而是约束最恰当的。
16.1 Context 的性能问题与 useSyncExternalStore
16.1.1 Context 的传播机制
在深入第三方状态管理库之前,我们必须先理解 React 内置方案的局限性。Context 的核心实现在 propagateContextChange 函数中:
typescript
// react-reconciler/src/ReactFiberNewContext.js
function propagateContextChange<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes
): void {
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
while (fiber !== null) {
let nextFiber: Fiber | null = null;
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dependency = list.firstContext;
while (dependency !== null) {
// 检查这个 Fiber 是否依赖了发生变化的 Context
if (dependency.context === context) {
// 找到了依赖此 Context 的消费者
if (fiber.tag === ClassComponent) {
const update = createUpdate(renderLanes);
update.tag = ForceUpdate;
enqueueUpdate(fiber, update, renderLanes);
}
// 关键操作:标记该 Fiber 需要更新
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// 向上冒泡 childLanes
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress
);
list.lanes = mergeLanes(list.lanes, renderLanes);
break;
}
dependency = dependency.next;
}
}
// 继续深度优先遍历
// ...省略遍历逻辑
fiber = nextFiber;
}
}这段代码揭示了 Context 性能问题的根源:当 Provider 的 value 发生变化时,React 必须遍历整个子树来找到所有消费者。这是一个 O(n) 的操作,其中 n 是 Provider 下的所有 Fiber 节点数量,而不仅仅是消费者的数量。
更致命的问题在于 Context 的更新粒度:
tsx
interface AppState {
theme: string;
locale: string;
user: User;
notifications: Notification[];
}
const AppContext = createContext<AppState>(defaultState);
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
// 当任何字段变化时,所有消费者都会重渲染
<AppContext.Provider value={state}>
<Header /> {/* 只用了 theme */}
<Sidebar /> {/* 只用了 notifications */}
<Content /> {/* 只用了 user */}
</AppContext.Provider>
);
}
function Header() {
// 即使只读取了 theme,当 notifications 变化时也会重渲染
const { theme } = useContext(AppContext);
return <header className={theme}>...</header>;
}16.1.2 changedBits:一个被废弃的优化尝试
很少有人知道,React 曾经尝试过一个叫做 changedBits 的 Context 优化方案。它出现在 React 16 的早期版本中,允许开发者指定哪些位发生了变化:
typescript
// 这是一个已被废弃的 API,仅作历史分析
const MyContext = createContext(defaultValue, (prev, next) => {
let changedBits = 0;
if (prev.theme !== next.theme) changedBits |= 0b01;
if (prev.locale !== next.locale) changedBits |= 0b10;
return changedBits;
});
// 消费者可以指定只关心哪些位
<MyContext.Consumer unstable_observedBits={0b01}>
{value => <div>{value.theme}</div>}
</MyContext.Consumer>这个方案最终被移除了。原因有三:第一,位运算限制了最多 31 个可追踪的字段;第二,它将 Context 的内部实现暴露给了用户,违背了 React 一贯的"声明式"设计哲学;第三,React 团队决定将细粒度订阅的职责交给用户空间的状态管理库,而不是在核心中实现一个必然不完善的方案。
🔥 深度洞察:React 的设计哲学是"做少而精的事"
Context 的性能问题不是一个 bug,而是一个有意识的设计取舍。React 团队选择让 Context 保持简单——它是一个依赖注入机制,不是一个状态管理系统。细粒度订阅、派生状态、中间件——这些功能属于用户空间,而不是框架核心。这个决策催生了繁荣的状态管理生态,也让每个库可以在各自的维度上做到极致。
16.1.3 useSyncExternalStore:并发安全的外部状态桥梁
React 18 引入并发渲染后,所有外部状态管理库都面临一个严峻的问题:tearing(撕裂)。在并发模式下,一次渲染可能被中断和恢复,如果外部状态在渲染过程中发生变化,不同组件可能读取到同一状态的不同版本,导致 UI 不一致。
useSyncExternalStore 就是为解决这个问题而设计的。它的源码实现比大多数人想象的要复杂得多:
typescript
// react-reconciler/src/ReactFiberHooks.js
function mountSyncExternalStore<T>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
const nextSnapshot = getSnapshot();
// 检测快照是否在渲染期间发生了变化(tearing 检测)
const root = getWorkInProgressRoot();
if (!includesBlockingLane(root, renderLanes)) {
// 非阻塞渲染(并发渲染)中,需要额外检查
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
hook.memoizedState = nextSnapshot;
const inst: StoreInstance<T> = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
// 使用 useEffect 订阅外部 store
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
// 使用 useEffect 检测 getSnapshot 或 value 的变化
mountEffect(
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
null // 每次渲染都执行
);
return nextSnapshot;
}这段代码中最关键的是 pushStoreConsistencyCheck。在并发渲染中,React 会在渲染完成后、提交之前,检查所有 useSyncExternalStore 消费者的快照是否仍然与当前外部状态一致:
typescript
function pushStoreConsistencyCheck<T>(
fiber: Fiber,
getSnapshot: () => T,
renderedSnapshot: T,
): void {
fiber.flags |= StoreConsistency;
const check: StoreConsistencyCheck<T> = {
getSnapshot,
value: renderedSnapshot,
};
// 挂载到当前渲染的根节点上
let checks = renderPhaseUpdates;
if (checks === null) {
checks = renderPhaseUpdates = [];
}
checks.push(check);
}