前言
通过阅读本文你能学到:在项目中使用一种轻量级的状态管理方案
自从步入了前后端分离时代,前端就多了一个「状态管理」的概念,这个是之前“Jquery时代”所没有的,因为之前的Jquery时期,没有所谓状态的概念,所有的数据需要自己手动从DOM上取,并且手动更新到DOM上去。
但是前后端分离之后,Vue、React一众框架都引入了“状态管理的”概念,框架使用者只需要维护状态,框架内部将自动完成 视图=>状态=>视图 的变更。
因此,现代前端的核心就是:状态管理。
核心:视图交互,及其它操作引起状态的变化,状态的变化又触发视图渲染、逻辑执行
graph TD
视图交互... --> 状态变化
状态变化 --> 视图渲染,逻辑执行
由此:状态管理可以被分为两个层面:
1.状态变化之前的逻辑,一般是异步的。
2.状态变化之后的联动处理,比如渲染视图或执行某段逻辑。
根据现代前端框架,状态管理的实现又可以被分为两种:
1. 提供API,内部进行联动处理。典型:React
2. 代理对象的set、get方法。get时收集所有依赖,set时做联动处理。典型:Vue
具体实现
这里以组件内部、组件之间、全局来进行分类。
组件内部
React:setState
Vue:用 Proxy 的 api 代理 状态的get、set。直接修改data即可
组件之间
React:Props、Context
Vue:Props、EventBus
全局
Redux、Vuex、Mobx等
问题
但是在实际项目中,我们要根据项目的实际情况来选用对应工具库,当我们的项目需要复杂的状态管理时,我们可以使用Redux、Vuex等工具。但是当我们项目没有复杂的状态管理或不想引入Redux\Vuex等重量级的状态管理库时,也就是:我们不需要E-100这样的超重型坦克,我们是不是可以有一把M249轻型机枪就可以解决项目问题呢?
E-100
M249
"M249"的优势:轻量级、门槛低、灵活。
"M249"全局状态管理
那么为你项目封装一个轻量级全局状态管理吧 GO~
deep-state-observer:一个高性能应用程序的状态管理库。
API不做过多介绍,地址:www.npmjs.com/package/dee…
我们从0开始使用deep-state-observer来为我们的项目封装一个全局状态管理
初始化项目&安装
使用脚手架创建React + TS项目
create-react-app.dev/docs/gettin…
安装依赖
npm i deep-state-observer
封装
在src目录下新建store.ts文件
第一步:导入deep-state-observer包
import type DeepStateType from "deep-state-observer";
import State, { ListenerFunction } from "deep-state-observer";
第二步:声明定义我们的Store接口
export interface MyStoreType {
store: DeepStateType;
/** 设置某个store的状态数据 */
set: (
namespace: string, //store的命名空间 存储层级需要复杂时可以使用,不然可以当作key使用
value: any,
key?: string | undefined | null
) => void;
/** 批量设置多个store的缓存 */
batchSet: (arg: { namespace: string; value: any }[]) => void;
/** 获取某个store的数据 */
get: (namespace: string, key?: string | undefined | null) => any;
/** 批量获取数据 */
batchGet: (arg: { namespace: string; key?: string }[]) => Record<string, any>;
/** 订阅某个store的变更 */
subscribe: (
namespace: string,
callback: ListenerFunction,
key?: string | undefined | null,
time?: number
) => () => void;
/** 批量订阅多个tore的变化 */
subscribeAll: (keys: string[], callback: ListenerFunction) => () => void;
}
第三步:实现接口
export default class MyStore implements MyStoreType {
store: any;
/**
* @description 初始化命名空间
*/
public init() {
this.createStore();
return this;
}
/**
* 创建全局store 默认使用1个内存空间
*/
public createStore() {
const storeJSON = {
// namespace: {...}
};
// 默认初始化一个空的state,需要时再向其中set即可。
this.store = new State(storeJSON);
}
/**
* 设置store的状态数据
* @param namespace
* @param key
* @param value
*/
public set = (
namespace: string,
value: any,
key?: string | undefined | null
) => {
let condition = namespace;
if (key) {
condition = `${condition}.${key}`;
}
this.store.update(condition, value);
};
/**
* 同于批量设置多个关键store的缓存
* @param params typeof array
*/
public batchSet = (params: { namespace: string; value: any }[]) => {
if (Array.isArray(params) && params.length > 0) {
params.map((it: any) => {
const { namespace, value, key } = it;
this.set(namespace, value, key);
});
}
};
/**
* 获取对应命名空间数据值
* @param namespace
* @param key
*/
public get = (namespace: string, key?: string | undefined | null) => {
let condition = namespace;
if (key) {
condition = `${condition}.${key}`;
}
return this.store.get(condition);
};
/**
* 批量获取数据
* @param params typeof array
* @returns
*/
public batchGet = (params: { namespace: string; key?: string }[]) => {
let tempJson: any = {};
if (Array.isArray(params) && params.length > 0) {
params.map((it: any) => {
const { namespace, key } = it;
tempJson[namespace] = this.get(namespace, key);
});
}
return tempJson;
};
/**
* 订阅变更
* @param namespace
* @param callback 数据变更回调
* @param key
*/
public subscribe = (
namespace: string,
callback: Function,
key: string | undefined | null
//可以根据业务需要加入订阅防抖,不过一般防抖都会加在数据的输入端,这里基本不用
// time?: number
) => {
let condition = namespace;
if (key) {
condition = `${condition}.${key}`;
}
return this.store.subscribe(condition, callback);
};
/**
* 批量订阅变更
* @param keys
* @param callback
*/
public subscribeAll = (keys: string[], callback: Function) => {
return this.store.subscribeAll(keys, callback);
};
}
初始化
我们可以在顶层组件中初始化我们的Store,demo项目中在App.tsx组件进行此操作
//App.tsx
// 初始化store并将其挂载到window上。
if (!window.MyStore) {
const MyStore = new MyStore();
window.MyStore = MyStore.init(),
}
const App() {
return (
...
)
}
食用
饭已就绪,米西米西~
现在,我们就可以在项目中的任意组件中使用我们的“M249”了。
创建大兵A: A.JSX
const A = () => {
const onClick = () => {
const data = {
name: "子弹",
count: 999
};
console.log("A:哒哒哒哒....", data);
//大兵A开枪了
window.MyStore.set("data", data);
};
return (
<div>
A士兵🪖
<div>
<img onClick={onClick} alt="开枪" />
</div>
</div>
);
};
export default A;
创建大兵B B.JSX
import { useEffect } from "react";
const B = () => {
useEffect(() => {
// 订阅数据变化
const unSub = window.MyStore.subscribe("data", (data: any) => {
console.log("B:啊! 我中弹了:", data);
});
return () => {
// 组件销毁时取消订阅
unSub?.();
};
}, []);
return <div>B士兵🪖</div>;
};
export default B;
实际战场中B和A“遥遥相望”,B甚至不认识A,也不知道他在哪里。 如果A、B是这个简单层级关系直接把"子弹交给"两者的上级就可以了,俗称:打小报告(Props)
我们可以看到,subscribe有一个参数是callback,通过阅读ListenerFunction以及update的类型定义可以知道:
callback的value参数就是我们封装的set函数的第二个参数value值。
export declare type ListenerFunction = (value: any, eventInfo: ListenerFunctionEventInfo) => void;
至此,我们就实现了两个组件的实时通信:当我们在A组件中进行了某些操作,想要立即告诉B组件,就可以在A组件中set("data001",...),而B组件在初始化后就对特定的data进行了订阅:subscribe("data001",...)。因此就可以在回调函数中收到来自A的通知。
场景
这里列举出一些场景来助大家食用:
- 用户信息
- 网站皮肤
- 局部数据刷新的flag
- 全局抽屉开关状态
- ...
由此可见,当你的A、B组件“遥遥相望”,一切“跨组件通信”的行为都可以使用它,但是在使用前请先考虑A、B的组件“关系”:"打小报告"(props、context)是否可以解决问题,如果解决不了,不好意思,直接掏出我的“M249”开干。
当然了,set\get等“单发组合”也可以作为缓存使用,甚至可以对我们的“M249”进行改装,按需开发其它能力。
对了,麦克阿瑟用了都说好!
总结
本文首先介绍了“状态管理”对于前端领域的重要性,接着认识了deep-state-observer,最后,基于它为我们的项目封装了一个轻量级全局状态管理工具。
源码
源码已经放在这里啦: