如何管理数据?
日常使用:发布订阅、context、redux…
zustand是一个轻量、快速、可扩展的状态管理库。
目前在社区非常流行,现在github上有30K+的star。npm包的下载量,现在也仅次于redux,位于mobx之上,并且差距日益扩大。
zustand 🇩🇪 德语 “状态”、jotai 🇯🇵 日语 “状态”、valtio 🇫🇮 芬兰语
“状态”,这三个都是状态管理库,作者是同一个人:Daishi Kato。
1 zustand over redux?
-
学习成本低:全面拥抱hooks,仅有1个核心api,无其它库依赖。
redux:学习曲线陡峭,大多数情况下需要配合其它库和中间件才能工作。 -
开发者体验好:无需Provider,无啰嗦的模板代码,异步处理就是普通的async/await。
redux:模板代码饱受诟病,action、actionType、reducer… 异步处理依赖中间,写法相对麻烦。 -
体积更轻量:1.1KB gzipped。
redux:redux(1.8KB) + react-redux(4.7KB) + redux-saga(5KB) = 11.5KB。
mobx:16.5KB。
recoil:23.5KB。
jotai:2.4KB。
valtio:3KB
2 如何使用
2.1创建一个store
import { create } from 'zustand';
interface CountState {
// 数值
count: number;
// 增加
increment: () => void;
// 减少
decrement: () => void;
}
// 创建初始state
const createInitStateFn = (set) => ({
count: 0, // 初始值
increment() {
set((state) => ({
count: state.count + 1,
}));
},
decrement() {
set((state) => ({
count: state.count - 1,
}));
},
});
// 创建状态存储
const useCountStore = create<CountState>(createInitStateFn);
2.2绑定组件
const Counter = () => {
const { count, increment, decrement } = useCountStore();
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
export default Counter;
2.3 状态派生
count=0 绿色
count<0 红色
count>0 蓝色
// selector
const colorSelector = (state) => {
if (state.count === 0) {
return 'green';
}
if (state.count < 0) {
return 'red';
}
return 'blue';
};
// Counter
const Counter = () => {
const { count, increment, decrement } = useCountStore();
const color = useCountStore(colorSelector);
return (
<div>
<p style={{ color }}>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
export default Counter;
2.4 中间件拓展
// 添加日志打印功能
const log = (config) => (set, get, api) =>
config(
(...args) => {
console.log(' applying', args)
set(...args)
console.log(' new state', get())
},
get,
api
)
const useCountStore = create(log(createInitStateFn))
2.5 实例:主接口数据管理
/*
* @Name: 全局数据管理
* @Date: 2023-08-13 10:00:41
* @author: xiaohongru.xhr
*/
import { create } from 'zustand';
import Service from '@/service';
import { query } from '@ali/tl-detector';
import { IHome } from '@/types';
const initialState: IHome = {} as IHome;
interface IGlobalStore {
isLoading: boolean;
isError: boolean;
homeData: IHome;
fetchHomeData: () => Promise<any>;
receiveAward: (type: string) => Promise<any>;
}
export const useGlobalStore = create<IGlobalStore>()((set, get) => ({
isLoading: true,
isError: false,
homeData: initialState,
// 查询主接口
fetchHomeData: () => {
return Service.queryHomeData({
fromSource: query?.fromSource,
})
.then((res) => {
set({ isLoading: false, isError: false, homeData: res?.result });
return Promise.resolve(res?.result);
})
.catch((err) => {
set({ isLoading: false, isError: true });
return Promise.resolve(err);
});
},
// 领取奖品接口
receiveAward: (type) => {
return Service.queryAward({
type,
})
.then((res) => {
// 领取奖励后刷主接口
get().fetchHomeData();
return Promise.resolve(res);
})
.catch((err) => {
return Promise.resolve(err);
});
},
}));
// 主接口状态
export const useIsError = () => useGlobalStore((state) => state.isError);
export const useIsLoading = () => useGlobalStore((state) => state.isLoading);
// 一些操作后需要重新刷主接口
export const useFetchHomeData = () => useGlobalStore((state) => state.fetchHomeData);
// 主接口数据
export const useHomeData = () => useGlobalStore((state) => state.homeData);
3 实现
3.1 一句话
zustand = 发布订阅 + react hooks
核心:create函数
import { create } from 'zustand';
const useCountStore = create(fn);
create传入参数是一个函数fn,返回值也是一个函数useCountStore
// 问题:如何处理入参数fn, 如何得到返回值
const create = (fn) => {
// 创建store,返回操作store的api
const api = isFunction(fn)? createStore(fn) : fn;
// 通过hooks 挂载到页面上
const useBoundStore = (seletctor, equalityFn) => {
return useStore(api, selector, equalityFn);
}
Object.assign(useBoundStore, api);
return useBoundStore;
}
3.3 createStore 发布订阅
- 构造一个store的结构
- 用fn创建一个store实例
const createStore = (fn) => {
let state;
const listeners: Set<Listener> = new Set();
// get
const getState = () => state;
// set
const setState = (partial: any, replace: boolean) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const prev = state;
state = replace ? nextState : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, prev));
}
};
// 添加订阅
const subscribe = (listener) => {
listeners.add(listener);
// Unsubscribe
return () => {
listeners.delete(listener);
};
};
// 清理订阅
const destroy = () => {
listeners.clear();
};
const api = { setState, getState, subscribe, destroy };
// 给state赋值
state = fn(setState, getState, api);
return api;
};
3.4 挂载状态
如何实现store中的值变成页面的state
// 绑定hooks
const useStore = (api, selector, equalityFn) =>{
const [state, setState] = useState(api.getState());
useEffect(()=>{
// state对象中的一个key的value变化,会改变整个对象地址,需要selector,进行优化
const unsubscribe = api.subscribe(state => setState(selector(state)));
return unbscribe;
},[])
return state;
}
使用react 新hooks:useSyncExternalStore实现
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T
- subscribe:注册回调的函数,返回一个() => void 用于清除副作用函数,每当 store 更改时调用该回调函数触发组件更新
- getSnapshot:返回对应(想要)的 store
- getServerSnapshot:返回服务器渲染期间使用的快照的函数,一般般用于 SSR 场景
// 用useSyncExternalStore实现useStore
const useStore=(api, selector, equalityFn)=>{
const value = useSyncExternalStore(api.subscribe, ()=> selector(api.getState()));
return value;
}
完整实现
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
const useStore = (api, selector, equalityFn) => {
// 针对equalityFn来对更新前后数据进行对比
const instRef = useRef(null);
let inst;
if (instRef.current === null) {
inst = {
hasValue: false,
value: null,
};
instRef.current = inst;
} else {
inst = instRef.current;
}
const _useMemo = useMemo(
function () {
let hasMemo = false;
let memoizedSnapshot;
let memoizedSelection;
const memoizedSelector = function (nextSnapshot) {
if (!hasMemo) {
// 第一次调用钩子时,没有记忆结果
hasMemo = true;
memoizedSnapshot = nextSnapshot;
var _nextSelection = selector(nextSnapshot);
if (equalityFn !== undefined) {
// 即使选择器已更改,当前呈现的选择也可能等于新选择。 如果可能的话,我们应该尝试重用当前值,以保留下游记忆
if (inst.hasValue) {
var currentSelection = inst.value;
if (equalityFn(currentSelection, _nextSelection)) {
memoizedSelection = currentSelection;
return currentSelection;
}
}
}
memoizedSelection = _nextSelection;
return _nextSelection;
}
const prevSnapshot = memoizedSnapshot;
const prevSelection = memoizedSelection;
if (Object.is(prevSnapshot, nextSnapshot)) {
// 快照与上次相同。 重复使用之前的选择
return prevSelection;
}
// 快照已更改,因此我们需要计算新的选择
const nextSelection = selector(nextSnapshot);
// 如果提供了自定义 equalFn 函数,请使用它来检查数据是否已更改。
// 如果没有,则返回之前的选择。
// 这向 React 发出信号,表明选择在概念上是相等的,我们可以摆脱渲染
if (
equalityFn !== undefined &&
equalityFn(prevSelection, nextSelection)
) {
return prevSelection;
}
memoizedSnapshot = nextSnapshot;
memoizedSelection = nextSelection;
return nextSelection;
};
const getSnapshotWithSelector = function () {
return memoizedSelector(api.getState());
};
return [getSnapshotWithSelector];
},
[api.getState, selector, equalityFn]
);
const getSelection = _useMemo[0];
// 挂在到state上
let value = useSyncExternalStore(api.subscribe, getSelection);
useEffect(() => {
inst.hasValue = true;
inst.value = value;
}, [value]);
return value;
};
4 总结
从页面视角更新过程:
参考资料
- zustand官网:https://awesomedevin.github.io/zustand-vue/docs/introduce/start/zustand
- useSyncExternalStore:https://react.dev/reference/react/useSyncExternalStore
- 学习 useSyncExternalStore:https://juejin.cn/post/7090063329913208868