手写工具-状态管理 STORE

手写工具-状态管理 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 讲的事件的发布订阅,按照这个思路我们梳理下大概流程:

  1. UI 组件对 store 数据修改进行订阅,可以 N 个 UI 组件进行订阅
  2. 我们修改 Store 数据触发修改事件
  3. 触发事件后,UI 组件中监听事件处理逻辑开始执行,修改自己的状态 State 或 Props,UI 状态被刷新,流程 Done

2.2 Store 简单写写先

按照上面事件流程,我们 Store 集中管理状态,修改状态触发修改事件,于是

2.2.1 试想一下 Store 使用方式

UI 组件做两件事情:

  1. 取需要的 store 数据进行初始化
  2. 订阅 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 里完成共享状态的修改事件触发。

试想一下共享数据变化流程:

  1. 实例化 store,初始化数据
  2. 在各个 UI 组件里使用 Store 数据,并对修改事件进行订阅
  3. 当我们执行 store.state1=1; 触发了 state1 的修改事件
  4. 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 代码所示,提供接口:

  1. 初始化 store
  2. store 修改订阅
  3. store 状态获取
  4. 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 的状态字段

  • 9
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据提供的代码, el-collapse 的过程如下所示: 1. 首先,需要创建一个 Collapse 组件的 Vue 单文件组件。 2. 在 template 中,使用 `<div>` 标签来包裹需要折叠的内容。可以使用 `v-for` 来遍历 `collapseData` 数组,并使用 `CollapseItem` 组件来展示每个折叠项的标题和内容。 3. 在 script 标签中,导出一个对象,其中包含了组件的相关属性和方法。 4. 在 data 中定义一个 `collapseData` 数组,用于存储每个折叠项的标题和内容。 5. 在 style 标签中,使用 scoped 属性来限定样式的作用域,可以定义 `.collapse` 类来设置组件的宽度。 下面是 el-collapse 的代码示例: ```vue <template> <div class="el-collapse"> <div v-for="(item, index) in collapseData" :key="index" class="el-collapse-item"> <div class="el-collapse-header" @click="toggleCollapse(index)"> {{ item.title }} </div> <div class="el-collapse-content" v-show="item.show"> <p>{{ item.content }}</p> </div> </div> </div> </template> <script> export default { data() { return { collapseData: [ { title: '标题1', content: '与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;', show: false }, { title: '标题2', content: '控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;', show: false }, { title: '标题3', content: '清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;', show: false }, { title: '标题4', content: '用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;', show: false }, ], }; }, methods: { toggleCollapse(index) { this.collapseData[index].show = !this.collapseData[index].show; }, }, }; </script> <style scoped> .el-collapse { width: 600px; } .el-collapse-item { margin-bottom: 10px; } .el-collapse-header { cursor: pointer; background-color: #f0f0f0; padding: 10px; } .el-collapse-content { padding: 10px; background-color: #fff; border: 1px solid #ccc; } </style> ``` 这样,你就可以使用 `<el-collapse>` 标签来引用这个组件,并实现风琴效果。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Vue个人组件库——Collapse / CollapseItem](https://blog.csdn.net/m0_46995864/article/details/118893487)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值