浅看状态管理新星 Jotai

1,754 阅读5分钟

浅看状态管理新星 Jotai

背景

目前主流的前端状态管理可以分为以下:

  • react context - 数据存储在树顶部,所有依赖的组件在值变化时均会强制更新,没有细化到具体某个属性变化
  • redux - 单向数据流和不可变状态模型的代表,值更新必须通过 action 写入,数据变更很清晰,不过存在上手难度高、大量模板代码、大状态量情况下性能较差等问题,不太适用于 react hooks方式
  • mobx - 函数响应式编程和可变状态模型的代表,使得状态管理更简单,提供了优化应用状态与 React 组件同步的机制,这种机制就是使用响应式虚拟依赖状态图表,它只有在真正需要的时候才更新并且永远保持是最新的;不过由于Mobx的响应式脱离了react自身的生命周期,就不得不显式声明其派生的作用时机和范围。比如副作用触发需要在useEffect里再跑一个autorun/reaction,要给DOM render包一层useObserver/Observer,都加大了开发成本。
  • recoil/jotai - 自下而上的原子化状态管理的代表,且也是不可变状态模型;每个状态均为一个原子(atom),类似于 react state,通过hooks和selector纯函数来组合、创建、更新。只有使用到该atom的组件才会在atom更新时触发re-render。因此无需定义模版代码和大幅改动组件设计,直接沿用类似于useState的API就能实现高性能的状态共享和代码分割。

以上罗列了一些目前社区中较为常用的状态管理方案,生产环境主流仍为 redux、mobx 这两位系列的老大哥,下文暂不阐述当前最优方案,将介绍一下原子化状态管理的新星 jotai 的相关使用。

基本信息

Primitive and flexible state management for React

根据官方定义可知,主打轻便灵活的原子化状态管理,且是针对于 react,所以它是与框架强相关的,不能脱离于 react 使用;

特性

  • Minimal core API (2kb)
  • Many utilities and integrations
  • No string keys (compared to Recoil)

官网

jotai.org/

npm

www.npmjs.com/package/jot…

基本 API

atom

定义原子状态,可以创建只读、只写、读写的原子以及派生状态;通过单个atom API的第一个参数来创建原始状态和派生状态,区别是后者参数是传入一个函数来对其他atom进行派生,第二个参数则用于生成指定更新函数的atom (writable derived atom和write only atom),使用起来非常简单。

import { atom } from 'jotai'const priceAtom = atom(10)
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
  null, // it's a convention to pass `null` for the first argument
  (get, set, update) => {
    // `update` is any single value we receive for updating this atom
    set(priceAtom, get(priceAtom) - update.discount)
  }
)
const readWriteAtom = atom(
  (get) => get(priceAtom) * 2,
  (get, set, newPrice) => {
    set(priceAtom, newPrice / 2)
  }
)

useAtom

获取定义的原子状态,可以像 useState 一样进行数组解构取值

const stableAtom = atom(0)
const Component = () => {
  // 因为 jotai 为 Object reference 需要保持引用唯一
  const [atomValue] = useAtom(atom(0)) // This will cause an infinite loop
  const [atomValue, setAtomValue] = useAtom(stableAtom) // This is fine
  const [derivedAtomValue] = useAtom(
    useMemo(
      // This is also fine
      () => atom((get) => get(stableAtom) * 2),
      []
    )
  )
}

示例


import { fetchLDAPUserInfo } from '@/common/apis';
import { atom } from 'jotai';
​
const priceAtom = atom(10);
​
// Create your atoms and derivatives
export const textAtom = atom('hello');
​
export const staticTextAtom = atom('this is static text');
​
export const uppercaseAtom = atom(get => get(textAtom).toUpperCase());
​
export const arrAtom = atom<string[]>(['1']);
​
// const writeOnlyAtom = atom(
//     null, // it's a convention to pass `null` for the first argument
//     (get, set, update) => {
//       // `update` is any single value we receive for updating this atom
//       set(priceAtom, get(priceAtom) - update.discount)
//     }
//   )export const readWriteAtom = atom(
    get => get(priceAtom) * 2,
    (get, set, newPrice: number) => {
        console.log(999, priceAtom, newPrice);
        set(priceAtom, newPrice / 2);
        // you can set as many atoms as you want at the same time
    }
);
​
export const asyncAtom = atom(async get => {
    // const res = await new Promise(resolve => {
    //     setTimeout(() => resolve(get(textAtom) + 666), 3000);
    // });
​
    const res = await fetchLDAPUserInfo();
    return res;
});

import React from 'react';
import { useAtom, useAtomValue } from 'jotai';
import { Button, Input, Space, Typography } from '@qunhe/muya-ui';
import {
    arrAtom,
    asyncAtom,
    readWriteAtom,
    staticTextAtom,
    textAtom,
    uppercaseAtom
} from './atoms';
​
// Use them anywhere in your app
const InputComp = () => {
    console.log(11111);
    const [text, setText] = useAtom(textAtom);
    const handleChange = e => setText(e.target.value);
    const [arr, setArr] = useAtom(arrAtom);
​
    return (
        <>
            <Input value={text} onChange={handleChange} />
            <Button type="primary" onClick={() => setArr([...arr, text])}>
                add
            </Button>
        </>
    );
};
​
const Uppercase = () => {
    console.log(22222);
    const [uppercase, setX] = useAtom(uppercaseAtom);
​
    // setX('7777777777777');
    return <div>Uppercase: {uppercase}</div>;
};
​
const ArrCaseComp = () => {
    console.log(33333);
    const [arr, setArr] = useAtom(arrAtom);
​
    return (
        <div>
            {arr.length} --- {arr.join('/')}
        </div>
    );
};
​
// Now you have the components
export const JotaiApp = () => {
    console.log(100000);
    const x = useAtomValue(staticTextAtom);
    const [y, setY] = useAtom(readWriteAtom);
    const [asyncRes] = useAtom(asyncAtom);
​
    setY(200);
​
    console.log(asyncRes);
​
    return (
        <Space direction="vertical" style={{ padding: 32 }}>
            <Typography.Title level={5}>{x}</Typography.Title>
            <InputComp />
            <Uppercase />
​
            <ArrCaseComp />
​
            <div>{y}</div>
            <div>{asyncRes.username}</div>
        </Space>
    );
};

实现原理

详细解读可查阅:blog.csdn.net/lecepin/art…

img]

jotai是基于 context 和订阅机制的产物,通过构建依赖图,达到类似 mobx 一样精准更新的目的。

使用时会把创建的所有 atoms 存在weakMap中挂载到 Provider 上,生成 atom 值与对应更新方法的映射关系,派生的 atom 也会生成一个监听器,一旦依赖的 atom 更新,就会从监听队列中取出来进行执行,从而触发对应组件的更新。

而当前主流的 Mobx ,它是将状态变成可观察数据,通过数据劫持,拦截其 get 来做依赖收集,知道每个组件依赖哪个状态。在状态的 set 阶段,通知依赖的每个组件重新渲染,做到了精准更新。

部分核心源码

export function atom<Value, Args extends unknown[], Result>(
  read: Value | Read<Value, SetAtom<Args, Result>>,
  write?: Write<Args, Result>
) {
  const key = `atom${++keyCount}`
  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
}
export const useStore = (options?: Options): Store => {
  const store = useContext(StoreContext)
  return options?.store || store || getDefaultStore()
}
​
export const Provider = ({
  children,
  store,
}: {
  children?: ReactNode
  store?: Store
}): FunctionComponentElement<{ value: Store | undefined }> => {
  const storeRef = useRef<Store>()
  if (!store && !storeRef.current) {
    storeRef.current = createStore()
  }
  return createElement(
    StoreContext.Provider,
    {
      value: store || storeRef.current,
    },
    children
  )
}
​
export const createStore = () => {
  const atomStateMap = new WeakMap<AnyAtom, AtomState>()
  let storeListeners: Set<StoreListener>
  
  const setAtomValue = <Value>(
    atom: Atom<Value>,
    value: Value,
    nextDependencies?: NextDependencies
  ): AtomState<Value> => {
    const prevAtomState = getAtomState(atom)
    const nextAtomState: AtomState<Value> = {
      d: prevAtomState?.d || new Map(),
      v: value,
    }
    if (nextDependencies) {
      updateDependencies(atom, nextAtomState, nextDependencies)
    }
    if (
      prevAtomState &&
      isEqualAtomValue(prevAtomState, nextAtomState) &&
      prevAtomState.d === nextAtomState.d
    ) {
      // bail out
      return prevAtomState
    }
    setAtomState(atom, nextAtomState)
    return nextAtomState
  }
  
 const subscribeAtom = (atom: AnyAtom, listener: () => void) => {
    const mounted = addAtom(atom)
    flushPending()
    const listeners = mounted.l
    listeners.add(listener)
​
    return () => {
      listeners.delete(listener)
      delAtom(atom)
    }
  }

本文由于篇幅暂不对源码进行解读,感兴趣可下载官方仓库源码查阅。

数据对比

主流下载量对比

下载数据对比

其他分析

npmtrends.com/jotai-vs-mo…

moiva.io/?npm=jotai+…