手写工具-状态管理 STORE
1.背景
在 React 里状态共享的基本方式是通过 props 流转到子组件,当一个组件不是子组件又希望共享状态时我们常规选项是 Redux 等,当我去用 Redux 时虽然能实现共享状态的需求,但是被 Redux 繁琐的概念和用法弄的一愣一愣,我只需要一个简单的集中共享状态,为啥使用起来这么复杂,Actions/Reducers/Dispatch 定义起来令人惆怅,于是我们自己愉快的写一个吧,Redux滚粗。如果有不想看过程的同学可直接看看Function组件使用Store和Class组件使用Store符不符合简单易用的预期
2.手写状态管理 MYSTORE
2.1 分析下要做的事情
上图为 Redux 图示动画,该做的还是这些,我们有个 store,当 store 中的数据修改了,UI 组件能感知到这个事情然后刷新显示。UI 组件感知到这个修改事件不就是前一篇浅析 EventTarget/EventEmitter 讲的事件的发布订阅,按照这个思路我们梳理下大概流程:
- UI 组件对 store 数据修改进行订阅,可以 N 个 UI 组件进行订阅
- 我们修改 Store 数据触发修改事件
- 触发事件后,UI 组件中监听事件处理逻辑开始执行,修改自己的状态 State 或 Props,UI 状态被刷新,流程 Done
2.2 Store 简单写写先
按照上面事件流程,我们 Store 集中管理状态,修改状态触发修改事件,于是
2.2.1 试想一下 Store 使用方式
UI 组件做两件事情:
- 取需要的 store 数据进行初始化
- 订阅 store 数据的变化
下面已 React 的 class 组件示意一下使用:
//实例化store
let store = new Store();
export class XXCompnent extends React.Component {
constructor(props) {
super(props);
this.state = {
//用store的state1初始化
state1: store.state1
}
//订阅store修改事件
store.On("state1", (ev: { oldState: number, newState: number }) => {
this.setState({ state1: ev.newState });
})
}
componentWillUnmount(): void {
//清理订阅事件,todo
//off一下修改事件
}
render(): React.ReactNode {
return <div></div>
}
}
试想一下如果多个组件参照上述方式共享使用 Store 的 state1,当我们修改 State1 是不是能够达到所有组件都同步修改,应该是能想到的吧
2.2.2 简单实现下 Store
Store 继承 EventEmitter 即可管理多个状态数据字段的修改事件的发布订阅,然后写些基本状态示意一下,
export class Store1 extends EventEmitter {
//假如我们共享状态1
private _state1: number;
set state1(value: number) {
let oldState = this._state1;
this._state1 = value;
//这里触发state1修改事件
this.emit("state1", { oldState, newState: value })
}
get state1() {
return this._state1;
}
//假如我们共享状态2
private _state2: string;
set state2(value: string) {
let oldState = this._state2;
this._state2 = value;
//这里触发state1修改事件
this.emit("state2", { oldState, newState: value })
}
get state2() {
return this._state2;
}
}
按照上图所示,Store 存在共享状态 state1 和 state2,我们在 Setter 里完成共享状态的修改事件触发。
试想一下共享数据变化流程:
- 实例化 store,初始化数据
- 在各个 UI 组件里使用 Store 数据,并对修改事件进行订阅
- 当我们执行
store.state1=1;
触发了 state1 的修改事件 - UI 组件订阅了 state1 事件,触发 setState 执行,完成 Render 刷新
如上面过程演示,基本的 store 应用已实现,但是使用上不够友好,包括:共享状态定义繁琐、UI 组件订阅/取消订阅繁琐
2.3 完善 Store 使其使用友好
2.3.1 简化 store 之状态定义与使用
这里我们通过 Proxy 完成每个字段状态的 get/set 钩子的触发,同上面的写法:
在 set 里完成 store 字段状态的修改,并触发相应字段修改事件;
get 返回 store 的具体字段状态;
//通用修改事件
interface IChangeEvent {
stateChange: { att: string, newValue: any, oldValue: any }
}
//单个修改事件
type IAttEvents<T extends object> = {
[key in keyof T]: { newValue: any, oldValue: any }
}
//未预先定义的字段
interface IDataEvents {
[k: string]: { newValue: any, oldValue: any }
}
export class Store<T extends object = {}> extends EventEmitter<IChangeEvent & IAttEvents<T> & IDataEvents> {
private _data: T;
private constructor(data?: T) {
super();
this._data = data ?? {} as T;
}
static create<T extends object>(data?: Partial<T>) {
let store = new Store(data);
let storedData = new Proxy(store, {
set: function (obj, prop, value) {
obj.set(prop as string, value)
return true;
},
get: function (obj, prop) {
return obj.get(prop)
}
});
return storedData as T & Store<T> & { [k: string]: any }
}
private set = (prop: any, value: any) => {
let oldValue = this._data[prop as keyof T];
this._data[prop as keyof T] = value as any;
this.emit("stateChange", { oldValue, newValue: value, att: prop });
this.emit(prop, { oldValue, newValue: value });
}
private get = (prop: any) => {
return this._data[prop as keyof T];
}
}
如上面 store 代码所示,提供接口:
- 初始化 store
- store 修改订阅
- store 状态获取
- store 状态修改
日常使用举例:
//共享状态
export class MyStates {
state1: string;
state2: number;
state3: number;
}
//初始化store,可初始化状态或者不传
let store = Store.create<MyStates>({ state1: "1", state2: 2 });
//store修改订阅,订阅单个状态state1修改事件
store.on("state1", (ev) => {
console.log(ev.newValue, ev.oldValue)
});
//store修改订阅,订阅单个状态state2修改事件
store.on("state2", (ev) => {
console.log(ev.newValue, ev.oldValue)
});
//store修改订阅,订阅通用修改事件
store.on("stateChange", (ev) => {
console.log(ev.att, ev.newValue, ev.oldValue)
})
//store修改订阅,订阅未预先定义的字段
store.on("state4", (ev) => {
console.log(ev.newValue, ev.oldValue)
})
//store状态获取,获取state4
let a=store.state4;
//store状态修改,修改state1
store.state1 = "2";
//store状态修改,修改state2
store.state2 = 3;
//store状态修改,修改state3
store.state3 = 3;
//store状态修改,设置未预先定义的字段
store.state4 = 5;
2.3.2 简化 Store 使用之 Class 组件-mapStoreToProps
class 组件使用可以继续按照 EventEmitter 的订阅/取消订阅去使用 Store,如果想简化,我们则可以封装一个 Class 类完成 Store 的状态的获取/订阅/取消订阅。
export function mapStoreToProps(store: Store, atts: string[]) {
return (Comp: Function) => {
return class extends React.Component {
private offList = [];
componentDidMount() {
this._debuffAction = DebuffAction.create();
let initState = {};
atts.forEach(item => initState[item] = store[item]);
this.setState({ ...initState });
let offList = [];
atts.forEach(item => {
let handler = (ev: { newValue: any, oldValue: any }) => {
let attState = {};
attState[item] = ev.newValue;
this.setState(attState);
};
store.on(item, handler);
offList.push(() => store.off(item, handler))
});
}
componentWillUnmount() {
this.offList.forEach(el => el())
}
render() {
let newProps = { ...this.props, ...this.state };
return (<Comp { ...newProps } />)
}
} as any
}
}
//自定义组件使用举例
如上面所示,我们通过挂装饰器的方式挂载 store 相关的处理逻辑,在装饰函数里起一个父组件完成 store 的相关处理,并将状态流转到子组件(UI 组件)props 里,这样 store 状态修改了,我们的 UI 组件即可以得到新的 props 状态完成组件展示刷新。
React 的 Class 组件使用举例:
@mapStoreToProps(store, ["state1","state2"])
export class XXComponent extends React.Component<{state1:string,state2:number}> {
render(): React.ReactNode {
return <div onClick=()=>{store.state2=5}>//修改store
<div>{this.props.state1}</div>//使用store字段
<div>{this.props.state2}</div>//使用store字段
</div>
}
}
如上面例子所示,组件使用共享 store 的 state1 和 state2
2.3.3 简化 Store 使用之 Class 组件- StoreComponent
在这里我们按照拓展基类组件功能,处理 Store 逻辑,在 UI 组件里继承基类组件即可
export class StoreComponent extends React.Component {
private _offList = [];
constructor(props: { store: Store, atts: string[] }) {
super(props);
let { store, atts } = props
let initState = {};
atts.forEach(item => initState[item] = store[item]);
this.setState({ ...initState });
atts.forEach(item => {
let handler = (ev: { newValue: any, oldValue: any }) => {
let attState = {};
attState[item] = ev.newValue;
this.setState(attState);
};
APP_STORE.on(item, handler);
this._offList.push(() => {
store.off(item, handler);
})
});
}
}
StoreComponent 在 React Class 组件使用举例:
export class XXXComponent extends StoreComponent {
constructor(props) {
super({ ...props, store, atts: ["state1", "state2"] });
}
render(): React.ReactNode {
return <div onClick={}>
<div>{this.state.state1}</div>
<div>{this.state.state2}</div>
</div>
}
}
如上面例子所示,在 constructor 里完成基类 Store 组件的传参,接下来我们在组件 State 里就有共享状态 State1 和 State2 了
2.3.4简化 Store 使用之 Function 组件- useStore
类似 useState 的使用方式,我们在 Usestore 里完成共享状态的获取,在 useEffect 里完成修改事件的订阅与取消订阅
export function useStore<T extends object, P extends keyof T>(store: T, attName: P): T[P] {
let [att, setAtt] = useState(store[attName as keyof T]);
useEffect(() => {
let handler = (ev: { newValue: any, oldValue: any }) => { setAtt(ev.newValue); }
store.on(attName as string, handler);
return () => {
store.off(attName as string, handler)
}
}, []);
return att
}
React 的 Function 组件使用举例:
export function XXComponent(){
let state1= useStore(store,"state1");
let state2= useStore(store,"state2");
return <div onClick=()=>{store.state2=5}>//修改store
<div>{this.props.state1}</div>//使用store字段
<div>{this.props.state2}</div>//使用store字段
</div>
}
2.4 Store 使用浅讲
点 1:常规项目可以定义一个全局 Store,这样在上面的简化 Function 里我们可以直接用全局 store,就可以不用进行 store 的传参,只需要在项目初始化完成 store 的初始化。
点 2:在我们 Store 是没有做状态字段的子属性修改触发修改事件逻辑,这个是考虑到如果组件需要监听状态字段的子属性修改,那就直接将该字段做为 store 的状态字段