从零实现一个简易版Redux,理解状态流背后的设计取舍
Redux 常被调侃为“一本说明书比代码还长”的库。但真去读它的源码,你会发现核心实现不过百来行。真正复杂的是它背后的设计取舍:为什么状态必须只读?为什么 reducer 必须是纯函数?为什么中间件长成洋葱模型?与其背 API,不如从零写一个 mini-redux,在实现过程中感受这些约束的来由。
核心三要素:store、action、reducer
Redux 的世界观很朴素:整个应用只有一个 store,store 里装着唯一的状态树。你不能直接修改状态,只能派发一个 action——一个描述“发生了什么”的普通对象。store 收到 action 后,会把它交给 reducer,由 reducer 决定新状态长什么样。
这三者的关系可以用一句话概括:`(state, action) => newState`。reducer 不是“修改”状态,而是“返回”新状态。这个看似微小的差别,决定了 Redux 的可预测性、可回溯性和可测试性。
手写一个 mini-redux
先实现最基础的 `createStore`:
function createStore(reducer, preloadedState) {
let state = preloadedState;
const listeners = [];
function getState() {
return state;
}
function dispatch(action) {
state = reducer(state, action);
listeners.forEach(fn => fn());
return action;
}
function subscribe(listener) {
listeners.push(listener);
return () => {
const i = listeners.indexOf(listener);
listeners.splice(i, 1);
};
}
dispatch({ type: '@@INIT' });
return { getState, dispatch, subscribe };
}
这就是 Redux 的骨架。它没有魔法,只是维护了一个闭包变量 `state` 和一个监听者数组。每次 dispatch 后,所有订阅者都会收到通知。至于通知后要不要重新渲染、怎么比较新旧状态,Redux 自己不管——这把选择权交给了 React-Redux、Vue 适配层或你自己。
为什么状态只读?纯函数 reducer 的取舍
Redux 强制 reducer 必须是纯函数:相同输入必须返回相同输出,不能有副作用。这带来两个直接好处:一是时间旅行调试成为可能,因为每次状态变化都是确定性的;二是测试变得极其简单,不需要 mock 任何东西。
但代价也很明显。纯函数意味着你不能在 reducer 里发请求、写日志、生成随机数。所有副作用都被赶到了 action 创建函数或中间件里。这增加了样板代码,也让新手容易写出“在 reducer 里调 API”的 bug。Redux 的选择是:用约束换可预测性,用样板换可维护性。
中间件:增强 dispatch 的洋葱模型
当需要处理异步或日志时,Redux 没有修改 store,而是提供了中间件机制。中间件的本质是“改写 dispatch”:
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error('禁止在构建中间件时调用 dispatch');
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
};
const chain = middlewares.map(mw => mw(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
};
}
`compose` 把多个中间件串成洋葱:请求从外到内穿透,响应从内到外返回。redux-thunk 让 dispatch 可以接收函数,redux-saga 则用 generator 管理复杂异步流。中间件没有破坏 store 的接口,却极大扩展了它的能力——这是 Redux 最优雅的设计之一。
订阅与性能:为什么 Redux 不内置响应式
Redux 的 `subscribe` 是“全量通知”,任何 action 都会触发所有监听者。它不关心你订阅的是哪一部分状态,也不做细粒度依赖追踪。这和 MobX、Vue 的响应式系统形成鲜明对比。
这种设计的好处是简单、可预测,状态变化路径完全显式;坏处是容易造成不必要的渲染。于是 React-Redux 不得不用 `connect` 或 `useSelector` 做浅比较,手动优化性能。Redux 把“状态管理”和“响应式更新”拆成两层,让核心保持极小,代价是生态层要补足更多心智负担。
单一数据源与可预测性的代价
单一 store 让调试、持久化、服务端渲染都变得容易。你可以把整个状态序列化到 localStorage,也可以把 action 日志发给监控平台。但全局状态树容易膨胀,模块间耦合也可能变高。于是又有了 `combineReducers`、Redux Toolkit 的切片方案,以及“按功能拆分 reducer”的社区共识。
从零实现之后会发现,Redux 的每一个设计都不是随意的:只读状态为了可预测,纯函数为了可测试,中间件为了可扩展,单一数据源为了可调试。它们共同构成了一套“约束换秩序”的哲学。理解这些取舍,比背熟 API 更重要——因为当你下次选择状态管理方案时,就能清楚知道自己在为什么买单。
转载请注明出处,版权归原作者所有。
管理员



