Skip to content

第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);
}

如果检查失败——即外部状态在渲染过程中发生了变化——React 会同步重新渲染整棵树,强制使用最新的快照。这个设计保证了一个关键特性:即使在并发模式下,UI 也永远不会出现撕裂。

这就是 useSyncExternalStore 名字中 "Sync" 的含义——它不是说订阅是同步的,而是说当检测到不一致时,它会强制同步渲染来确保一致性。

16.1.4 useSyncExternalStore 的使用模式

理解了内核实现后,useSyncExternalStore 的三个参数就不再神秘了:

typescript
function useSyncExternalStore<T>(
  // 订阅函数:当外部状态变化时调用 callback
  subscribe: (callback: () => void) => () => void,
  // 获取当前快照:必须返回不可变值(或缓存的引用)
  getSnapshot: () => T,
  // SSR 时使用的快照(可选)
  getServerSnapshot?: () => T
): T;

一个最小实现的外部 store 如下:

typescript
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<() => void>();

  return {
    getState: () => state,
    setState: (nextState: T | ((prev: T) => T)) => {
      state = typeof nextState === 'function'
        ? (nextState as (prev: T) => T)(state)
        : nextState;
      // 通知所有订阅者
      listeners.forEach(listener => listener());
    },
    subscribe: (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// 在 React 组件中使用
const counterStore = createStore({ count: 0 });

function Counter() {
  const state = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState
  );

  return <div>{state.count}</div>;
}

注意一个关键约束:getSnapshot 的返回值必须满足引用稳定性要求。如果每次调用都返回一个新对象,即使内容相同,也会触发无限重渲染。这正是许多状态管理库内部使用 selector + shallow comparison 的原因。

16.2 Redux Toolkit 与 React 19 的协作模式

16.2.1 Redux 的核心模型

在深入 Redux Toolkit 之前,让我们回到 Redux 最原始的核心——createStore。整个 Redux 的核心实现只有不到 100 行:

typescript
// redux/src/createStore.ts(简化版,保留核心逻辑)
function createStore<S, A extends Action>(
  reducer: Reducer<S, A>,
  preloadedState?: S,
  enhancer?: StoreEnhancer
): Store<S, A> {
  // enhancer 是一个高阶函数,用于扩展 createStore 的能力
  if (typeof enhancer !== 'undefined') {
    return enhancer(createStore)(reducer, preloadedState);
  }

  let currentReducer = reducer;
  let currentState = preloadedState as S;
  let currentListeners: (() => void)[] | null = [];
  let nextListeners = currentListeners;
  let isDispatching = false;

  function getState(): S {
    if (isDispatching) {
      throw new Error('不能在 reducer 执行过程中调用 getState');
    }
    return currentState;
  }

  function subscribe(listener: () => void): () => void {
    if (isDispatching) {
      throw new Error('不能在 reducer 执行过程中调用 subscribe');
    }

    let isSubscribed = true;

    // 关键设计:写时复制(Copy-on-Write)
    ensureCanMutateNextListeners();
    nextListeners.push(listener);

    return function unsubscribe() {
      if (!isSubscribed) return;
      isSubscribed = false;
      ensureCanMutateNextListeners();
      const index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1);
      currentListeners = null;
    };
  }

  function dispatch(action: A): A {
    if (isDispatching) {
      throw new Error('Reducer 不允许 dispatch');
    }

    try {
      isDispatching = true;
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    // 通知所有监听者
    const listeners = (currentListeners = nextListeners);
    for (let i = 0; i < listeners.length; i++) {
      listeners[i]();
    }

    return action;
  }

  // 初始化:通过一个特殊 action 让 reducer 返回初始状态
  dispatch({ type: '@@redux/INIT' } as any);

  return { dispatch, subscribe, getState };
}

仔细观察 subscribe 的实现——它使用了**写时复制(Copy-on-Write)**模式。nextListenerscurrentListeners 是两个独立的数组,只有当需要修改时才会创建副本。这个设计确保了在 dispatch 触发通知的过程中,即使有新的 subscribe/unsubscribe 操作,也不会影响当前正在遍历的监听者列表。

16.2.2 中间件链:compose 与 applyMiddleware

Redux 中间件是其最优雅的设计之一。理解中间件链的关键在于理解两个函数:composeapplyMiddleware

typescript
// compose:从右到左组合函数
// compose(f, g, h) 等价于 (...args) => f(g(h(...args)))
function compose(...funcs: Function[]): Function {
  if (funcs.length === 0) {
    return <T>(arg: T) => arg;
  }
  if (funcs.length === 1) {
    return funcs[0];
  }
  return funcs.reduce(
    (a, b) =>
      (...args: any) =>
        a(b(...args))
  );
}

compose 的实现只有一行核心代码,但它是理解中间件链的关键。让我们看看 applyMiddleware 如何使用它:

typescript
function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);

    let dispatch: Dispatch = () => {
      throw new Error('不允许在中间件构建过程中 dispatch');
    };

    // middlewareAPI 是每个中间件接收的参数
    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args),
    };

    // 第一步:让每个中间件访问 store API
    const chain = middlewares.map(middleware => middleware(middlewareAPI));

    // 第二步:通过 compose 将中间件串联成一条链
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}

这里有一个精妙的设计:middlewareAPI.dispatch 使用了闭包引用,而不是直接引用 store.dispatch。这意味着中间件通过 middlewareAPI 调用 dispatch 时,调用的是经过整个中间件链增强后的 dispatch。这个"自引用"的设计是 redux-thunk 等异步中间件能够工作的基础。

让我们用一个具体例子展开中间件链的执行过程:

typescript
// 一个典型的中间件签名
// middleware: (api) => (next) => (action) => result
const logger: Middleware = (api) => (next) => (action) => {
  console.log('dispatching', action);
  const result = next(action);
  console.log('next state', api.getState());
  return result;
};

const thunk: Middleware = (api) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(api.dispatch, api.getState);
  }
  return next(action);
};

// applyMiddleware(thunk, logger) 的执行链:
// dispatch(action)
//   → thunk(action)  — 如果 action 是函数,调用它
//     → logger(action) — 打印日志
//       → store.dispatch(action) — 实际的 reducer 执行

基于 VitePress 构建