一个轻量级全局状态管理

159 阅读6分钟

前言

通过阅读本文你能学到:在项目中使用一种轻量级的状态管理方案

自从步入了前后端分离时代,前端就多了一个「状态管理」的概念,这个是之前“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 image.png
M249 image.png

"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”了。 image.png

创建大兵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”开干。 image.png
当然了,set\get等“单发组合”也可以作为缓存使用,甚至可以对我们的“M249”进行改装,按需开发其它能力。 对了,麦克阿瑟用了都说好!image.png

总结

本文首先介绍了“状态管理”对于前端领域的重要性,接着认识了deep-state-observer,最后,基于它为我们的项目封装了一个轻量级全局状态管理工具。

源码

源码已经放在这里啦:

codesandbox.io/s/fancy-fra…

参考文档

www.npmjs.com/package/dee…

github.com/neuronetio/…