关于「你不知道的 Redux」这个专栏
在前端进阶中,redux 或者说是状态管理 是一个比较好的切入点,在实际项目中应用广泛,可选的解决方案非常多,生态非常丰富,学习他可以帮助我们提升编程设计思想,而且代码非常易读,代码量也不大,不像 webpack,react 源码那样庞大,学习收益非常高,同时也是面试中的高频考点。 专栏会持续更新 redux 整个生态的相关知识,包括 react-redux ,redux 中间件,immer 等等,甚至会考虑其他的解决方案,比如最近的 zustand 等。
- 第一篇 Redux toolkit 可能是 Redux 的最佳实践
- 第二篇 Redux toolkit 如何优雅的实现范式化
- 第三篇 React-Redux 那些年 Redux 官方开的小灶
- 第四篇 Redux 的泡面拍档 Immer.js
Why Zustand
首先列一个表格,对比 zustand 和 Redux 的优劣
| 特性/概念 | Zustand | Redux |
|---|---|---|
| API 复杂度 | 简洁,专为 hooks 设计 | 较为复杂,需要 action types, action creators, reducers |
| 使用方式 | 使用 hooks 直接管理状态 | 需要通过 Provider 连接整个应用,并且使用 connect 来连接组件 |
| 样板代码 | 几乎没有样板代码 | 需要编写样板代码,如 action types 和 reducers |
| 集成 React | 直接作为 hook 使用 | 需要 react-redux 库来与 React 集成 |
| 性能优化 | 内置 selector 支持,自动记忆化 | 通常需要使用 reselect 库来实现记忆化 |
| 异步处理 | 在 actions 中直接返回 Promise | 需要中间件如 redux-thunk 或 redux-saga 来处理异步逻辑 |
| 状态持久化 | 提供 persist 中间件支持 | 需要额外的库或自定义逻辑来实现状态持久化 |
| 社区与生态 | 相对较新,但增长迅速 | 拥有庞大的社区和成熟的生态系统 |
| 测试 | 易于编写单元测试 | 可以进行单元测试,但可能更繁琐 |
| bundle 大小 | 体积较小 | 由于功能更全,打包体积相对较大 |
| 学习曲线 | 对新手友好,易于上手 | 学习曲线较陡峭,需要理解更多概念 |
| 灵活性 | 支持创建多个 store 分离不同状态管理 | 通常使用单一 store 结构,虽然支持 reducer composition,但在某些场景下可能不够灵活 |
How to Use
首先简单实现一个 Counter组件
import create from 'zustand';
const useCounter = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
function Counter() {
const { count, increment, decrement, reset } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Counter;
设计理念
Redux 完全遵守了 Flux 的设计理念,所有的变更都通过 action 来触发,然后由 reducer 来处理。这种设计模式的优点是,所有的状态变更都是可预测的,因为它们都是同步的。但是,这也意味着异步操作会变得复杂,因为你需要使用中间件来处理异步操作。
而 Zustand 的思路是一切从简。它直接将状态管理器作为一个 hook,让你可以直接在组件中使用。甚至将修改 store 的 setState 方法暴露出来,可以直接在组件中修改 store 的状态
核心代码实现
实现 vanilla store 的核心能力
标准的观察者模式,对外暴露了 subscribe, getState, setState, destroy 四个方法。其中 setState 方法接受一个 partial state 或者一个函数,用于更新 store 的状态, 并且通知所有的订阅者
export const createStore = (createState) => {
let state = {},
initState;
const listeners = new Set();
const getInitialState = () => initState;
const getState = () => state;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const destroy = () => listeners.clear();
const setState = (partial) => {
let prevState;
if (typeof partial === "function") {
state = { ...state, ...partial(state) };
} else {
state = { ...state, ...partial };
}
listeners.forEach((listener) => listener(state, prevState));
};
const store = { setState, getState, getInitialState, subscribe, destroy };
state = initState = createState(setState, getState, store);
return store;
};
创建一个 useStore hook
利用 useSyncExternalStoreWithSelector 订阅了外部 Store 的状态。当 store 变更时引起了组件的重新渲染
import useSyncExternalStoreExports from "use-sync-external-store/shim/with-selector";
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
export const useStore = (store, selector = (args) => args, equalityFn) => {
const useBoundStore = useSyncExternalStoreWithSelector(
store.subscribe,
store.getState,
store.getInitialState,
selector,
equalityFn
);
Object.assign(useBoundStore, store); // 在这里暴露 store的相关方法
return useBoundStore;
};
实现 useStore 的工厂方法(create)
用于创建一个 store 的 hook,接受一个 store 的工厂方法,返回一个 useStore 的 hook
export const create = (createStore) => {
return (createState, selector, equalityFn) => {
const store = createStore(createState);
return useStore(store, selector, equalityFn);
};
};
总结
- 可以看到有了 useSyncExternalStoreWithSelector 之后,我们可以很方便的实现一个简单的 zustand。关于 useSyncExternalStoreWithSelector 的实现,可以参考我之前的文章 React-Redux 那些年 Redux 官方开的小灶
- Redux 期望通过Flux 数据流的方式来保证状态的可预测性,但是确牺牲了开发效率,而 Zustand 更加追求简洁。两种状态管理库各有优劣,具体使用哪个取决于项目的需求复杂度