浅看状态管理新星 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)
官网
npm
基本 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…
]
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)
}
}
本文由于篇幅暂不对源码进行解读,感兴趣可下载官方仓库源码查阅。