React 状态管理方案 (四)· jotai

985 阅读4分钟

状态管理的学习又续上了,死磕 React 状态管理到底! jotai 现在也获得了很多人的喜爱,是 React-context 的完善版(解决 Provider 嵌套和 re-render),是 Recoil 的简化版。和 Recoil 理念一致,都是“原子化”。现在开始!!! 完整示例

特点

用起来感觉(和 zustand 比好的 ✔️,差的 ✖️)

  • 🤗 与时俱进:积极拥抱 hooks
  • 🫧 简洁无比:不需要使用 context providers 包裹应用、✔️ 不用 selector 映射 state
  • 😊 门槛低:心智负担比较低 atom、useAtom 够用,又神似 useState
  • ⚛️ 原子化:✔️ 天然 re-render 优化,不需要借助外部在组件中手动处理
  • 💝 可以持久化(存到 localStorage)
  • 🔌 可扩展(插入中间件)
  • 🚧 ✖️ 外部不可用,只能组件中用
  • 📑 没有 action 的概念,✖️ 复用性差一些

原理架构上

与 redux、zustand 完全不同的设计理念,基于 react-context 的穿透能力把状态以原子形式分散到 React 内部全局。而 Zustand 状态 在 React 之外的 store 中,通过 hook 进行外部与 React 的连接。

  • atom.ts 导出 atom 方法,用来存入状态配置即“原子”。
  • store.ts 是实际的状态管理层,实现“原子”到状态的映射,管理状态之间的依赖关系,实现状态的增删改逻辑。向外部暴露状态的读 get、写 set、订阅 sub 的方法。
  • useAtomValue.ts 执行 sub 注册 listener,通过 rerender 对组件进行重渲染。
  • useSetAtom.ts 主要逻辑就是返回 store.set 的能力。
  • useAtom.ts 就是返回 [useAtomValue(),useSetAtom()] 了啦。

使用

类型

atom 可以细分为以下几种类型:

jotai-atom-types.png 具体区别要结合下面定义和使用 atom 的代码来看

定义 atom

每一种 atom 定义的时候都有一个固定的格式,具体看注释

  • todoStore.js
import { atom } from "jotai";

/* 原生 */
export const todoListState = atom([]);
/* 只读 */
export const todoTotalState = atom((get) => get(todoListState).length);
/* 只写 */
export const todoListQuery = atom(null, async (get, set, payload) => {
  const list = await queryMockList();
  set(todoListState, list);
});
/* 读写 */
export const testDriveReadAndWrite = atom(
  (get) => {},
  async (get, set, payload) => {}
);

使用 atom

使用的时候也有固定格式,具体看注释

  • List 列表组件
const List = () => {
  console.log("=======list render ======");
  /* 原生atom,使用时类似useState,
  只需要set函数的时候建议用 useSetAtom,
  只需要值的时候可以用 useAtomValue */
  const [todoList, setTodoList] = useAtom(todoListState);
  /* 只读的衍生atom,用法 */
  // const [todoTotal] = useAtom(todoTotalState); //也ok
  const todoTotal = useAtomValue(todoTotalState);
  /* 只写的衍生atom,用法 */
  // const [_,queryTodoList] = useAtom(queryTodoList); //也ok
  const queryTodoList = useSetAtom(todoListQuery);
  const deleteTodoItem = (id) => {
    setTodoList(todoList.filter((item) => item.id !== id));
  };
  const editTodoItem = (id) => {
    setTodoList(todoList.map((item) => (item.id === id ? { ...item, content: "random" } : { ...item })));
  };
  useEffect(() => {
    console.log("======= list useEffect []   ======");
    queryTodoList();
  }, []);

  return (
    <section>
      <!-- 省略,可看完整示例 -->
      <!-- 对todo进行删除、修改操作 -->
    </section>
  );
};
  • Header 用来新增 todo
const HeaderOne = () => {
  console.log("=======HeaderOne render ======");
  // const [todoList, setTodoList] = useAtom(todoListState); //会触发 HeaderOne render
  const setTodoList = useSetAtom(todoListState); //HeaderOne 不 render, 起到性能优化的作用

  const addTodo = () => {
    const content = document.getElementById("todo-input").value;
    setTodoList((prev) => [...prev, { content, id: new Date().getTime() }]);
  };
  return (
    <section>
      <input id="todo-input" type="text" />
      <button onClick={addTodo}></button>
    </section>
  );
};
  • Root
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
  <StrictMode>
    <div className="App">
      <h1>使用了jotai</h1>
      <h2>添加一个待办</h2>
      <HeaderOne />
      <Suspense fallback={<div>加载中。。。</div>}>
        <h2>待办列表</h2>

        <List />
      </Suspense>
    </div>
  </StrictMode>
);

源码分析

学艺不精呀,只能了解一个粗略的流程,没办法实现一个简易版的 jotai

  1. 调用 atom
    • 接收两个参数,分别用于 ① 读取/初始状态 ② 写入状态相关(下文简称 readFn/initialValue,writeFn)
    • 得到一个状态配置,下文简称 “原子”
export function atom<Value, Args extends unknown[], Result>(
  read: Value | Read<Value, SetAtom<Args, Result>>,
  write?: Write<Args, Result>
) {
  const key = `atom${++keyCount}`; //不需要像 Recoil 那样指定 key, 内部生成
  const config = {
    toString: () => key,
  } as WritableAtom<Value, Args, Result> & { init?: Value };
  if (typeof read === "function") {
    config.read = read as Read<Value, SetAtom<Args, Result>>;
  } else {
    config.init = read;
    config.read = (get) => get(config);
    config.write = ((get: Getter, set: Setter, arg: SetStateAction<Value>) =>
      set(
        config as unknown as PrimitiveAtom<Value>,
        typeof arg === "function" ? (arg as (prev: Value) => Value)(get(config)) : arg
      )) as unknown as Write<Args, Result>;
  }
  if (write) {
    config.write = write;
  }
  return config; // 返回配置
}
  1. 组件中调用 useAtom 或 useAtomValue 或 useSetAtom
    • 传入刚刚定义的“原子”
  1. 内部经过 useAtomValue/useSetAtom/依次调用 useAtomValue 和 useSetAtom -> useStore -> useContext 的逐层调用拿到 store(真正的状态容器)
  2. 如果 store 里没数据,说明是首次调用,继续调用 createStore 进行 store 的初始化;否则直接到 step 6.
  3. store 中用 WeakMap 这种数据类型来维护原子到状态的映射,store 暴露给外部 get、set、sub 方法
export const createStore = () => {
  const atomStateMap = new WeakMap<AnyAtom, AtomState>();
  const mountedMap = new WeakMap<AnyAtom, Mounted>();
  const pendingMap = new Map<AnyAtom, AtomState /* prevAtomState */ | undefined>();
  let storeListenersRev1: Set<StoreListenerRev1>;
  let storeListenersRev2: Set<StoreListenerRev2>;
  let mountedAtoms: MountedAtoms;
  //...
  // readAtom、getAtomState、writeAtom、setAtomState、subscribeAtom 等操作 store 的方法
  //...
  return {
    get: readAtom,
    set: writeAtom,
    sub: subscribeAtom,
  };
};
  1. 拿到有数据的 store ,如果组件中调用的是 useAtomValue 则通过 store.get 拿到状态的值;如果组件中调用的是 useSetAtom 则返回 store.set ;如果调用的是 useAtom 则依次进行前两种逻辑
export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
  const store = useStore(options);
  const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] = useReducer<
    ReducerWithoutAction<readonly [Value, Store, typeof atom]>,
    undefined
  >(
    (prev) => {
      const nextValue = store.get(atom);
      if (Object.is(prev[0], nextValue) && prev[1] === store && prev[2] === atom) {
        return prev;
      }
      return [nextValue, store, atom];
    },
    undefined,
    () => [store.get(atom), store, atom]
  );
  //...
  useEffect(() => {
    const unsub = store.sub(atom, () => {
      if (typeof delay === "number") {
        // delay rerendering to wait a promise possibly to resolve
        setTimeout(rerender, delay);
        return;
      }
      rerender();
    });
    rerender();
    return unsub;
  }, [store, atom, delay]);
  //...
}
export function useSetAtom<Value, Args extends any[], Result>(
  atom: WritableAtom<Value, Args, Result>,
  options?: Options
) {
  const store = useStore(options);
  const setAtom = useCallback(
    (...args: Args) => {
      return store.set(atom, ...args);
    },
    [store, atom]
  );
  return setAtom;
}
    • 到这里 const[value,setValue] = useAtom(xxx) 的 value 和 setValue 都拿到了
  1. 组件中调用 setValue 改变了状态,内部经过 store.set -> writeAtom -> writeAtomState -> atom.write 的逐层调用改变状态

    store.ts 中的逻辑是最复杂的,也是代码量最多的一个文件,不过作为使用者基本不用关心内部实现,因为我们是没有直接调用的 store 的。

  2. 更新