10 分钟 40 行代码帮你实现一个简单的 zustand

1,388 阅读4分钟

关于「你不知道的 Redux」这个专栏

在前端进阶中,redux 或者说是状态管理 是一个比较好的切入点,在实际项目中应用广泛,可选的解决方案非常多,生态非常丰富,学习他可以帮助我们提升编程设计思想,而且代码非常易读,代码量也不大,不像 webpack,react 源码那样庞大,学习收益非常高,同时也是面试中的高频考点。 专栏会持续更新 redux 整个生态的相关知识,包括 react-redux ,redux 中间件,immer 等等,甚至会考虑其他的解决方案,比如最近的 zustand 等。

Why Zustand

首先列一个表格,对比 zustand 和 Redux 的优劣

特性/概念ZustandRedux
API 复杂度简洁,专为 hooks 设计较为复杂,需要 action types, action creators, reducers
使用方式使用 hooks 直接管理状态需要通过 Provider 连接整个应用,并且使用 connect 来连接组件
样板代码几乎没有样板代码需要编写样板代码,如 action types 和 reducers
集成 React直接作为 hook 使用需要 react-redux 库来与 React 集成
性能优化内置 selector 支持,自动记忆化通常需要使用 reselect 库来实现记忆化
异步处理在 actions 中直接返回 Promise需要中间件如 redux-thunkredux-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 来处理。这种设计模式的优点是,所有的状态变更都是可预测的,因为它们都是同步的。但是,这也意味着异步操作会变得复杂,因为你需要使用中间件来处理异步操作。

Fluxshu流

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

总结

  1. 可以看到有了 useSyncExternalStoreWithSelector 之后,我们可以很方便的实现一个简单的 zustand。关于 useSyncExternalStoreWithSelector 的实现,可以参考我之前的文章 React-Redux 那些年 Redux 官方开的小灶
  2. Redux 期望通过Flux 数据流的方式来保证状态的可预测性,但是确牺牲了开发效率,而 Zustand 更加追求简洁。两种状态管理库各有优劣,具体使用哪个取决于项目的需求复杂度