zoukankan      html  css  js  c++  java
  • 摒弃 react-redux: 非侵入式状态共享实现

    前言

      众所周知,Redux 解决了组件之间数据交换问题,并提供了一系列插件可对应用监控和调试等。

      就 Redux 本身而言并不存在侵入性,而是 react-redux 广泛使用 connect 导致对组件的产生侵入性

      尽管 Hooks API 的 useSelector 和 useDispatch 已经化 n 数量级的侵入性为 1 —— <Provider/> 组件对 React 根应用的侵入性,仍然存在侵入性

      另一方面也对其使用 reducer + action + payload 的方式将原本组件内部独立功能分离感到十分没有必要

      所以决定设计并实现一个『发布订阅模块』,满足通用抽象的数据交换需求。

      ( 为什么不叫『状态共享模块』?因为本人不使用 reducer + action + payload 的模式我不敢叫别人一样的称号 )

      原本打算使用 redux 作为底层实现,考虑到业务需求就暂时只是用 JS 内置的数据结构,日后也容易对比自己与 Redux 团队的差距

     

    设计和实现

      这里的实现方式依据我早前开发的前端模块系统的约定,由于 github 网络时常不稳定,前两天将前端模块系统项目迁移到 getee:

      https://gitee.com/jhxlhl1023/mdl-00-codebase-frontend

      

    import { consts } from "@/module-00-codebase/pkg-00-const";
    import { Bi } from "@/module-00-codebase/pkg-01-container";
    import { BaseElmt } from "@/module-00-codebase/pkg-03-abstract";
    
    export class Broker {
      public read(propertyPath: string): any;
      public read(propertyPath: string, subscribeFuncKey: any, subscribeFunc: () => void): any;
      public read(propertyPath: string, subscribeFuncKey?: any, subscribeFuncCall?: () => void): any {
        let properties: string[];
        if (currentBindingElmt === null && !(subscribeFuncKey && subscribeFuncCall)) {
          // skip
        } else if ((properties = Bi.utils.splitProperties(propertyPath)).length === 0) {
          throw new Error("Cannot read value from Broker with an empty key.");
        } else {
          let parent = map;
          let node: SubscribeNode;
          for (let i = 0, length = properties.length; i < length; i++) {
            const tempNode = parent.get(properties[i]);
            if (!tempNode) {
              node = { key: properties[i], elmtSet: new Set(), functionMap: new Map(), leafs: new Map() };
              parent.set(properties[i], node);
            } else {
              node = tempNode;
            }
            parent = node.leafs;
            if (i === length - 1) {
              if (subscribeFuncKey && subscribeFuncCall) {
                node.functionMap.set(subscribeFuncKey, subscribeFuncCall);
              } else if (currentBindingElmt) {
                node.elmtSet.add(currentBindingElmt);
              }
            }
          }
          return Bi.utils.readValue(cache, propertyPath);
        }
      }
      public write(propertyPath: string, value: any): any {
        Bi.utils.writeValue(cache, propertyPath, value);
        const keys = Bi.utils.splitProperties(propertyPath);
        const elmtSet = new Set<BaseElmt<any>>();
        const funcSet = new Set<() => void>();
        const parent = map;
        let nodeOfKey: SubscribeNode | undefined = undefined;
        debugger;
        for (let i = 0, length = keys.length; i < length; i++) {
          const node = parent.get(propertyPath);
          if (!node) break;
          else addToSet(node, elmtSet, funcSet);
          if (i === keys.length - 1) nodeOfKey = node;
        }
        if (!!nodeOfKey) addToSetCascade(nodeOfKey, elmtSet, funcSet);
        funcSet.forEach(func => func.call(null));
        elmtSet.forEach(elmt => elmt.elmtRefresh());
      }
    }
    export const initializing = async () => {
      const oldRender = BaseElmt.prototype.render;
      const newRender = function (this: BaseElmt<any>) {
        const oriElmt = currentBindingElmt;
        currentBindingElmt = this;
        try {
          return oldRender.call(this);
        } finally {
          currentBindingElmt = oriElmt;
        }
      };
      BaseElmt.prototype.render = newRender;
    };
    export const order = () => consts.firstOrder;
    const addToSetCascade = (node: SubscribeNode, elmtSet: Set<BaseElmt<any, any>>, funcSet: Set<() => void>) => {
      addToSet(node, elmtSet, funcSet);
      node.leafs.forEach(node => addToSetCascade(node, elmtSet, funcSet));
    };
    const addToSet = (node: SubscribeNode, elmtSet: Set<BaseElmt<any, any>>, funcSet: Set<() => void>) => {
      node.elmtSet.forEach(elmt => (elmt.isDestroyed ? node.elmtSet.delete(elmt) : elmtSet.add(elmt)));
      node.functionMap.forEach(func => funcSet.add(func));
    };
    const cache = {} as any;
    const map = new Map<string, SubscribeNode>();
    let currentBindingElmt: BaseElmt<any> | null = null;
    
    type SubscribeNode = { key: string; elmtSet: Set<BaseElmt<any>>; functionMap: Map<any, () => void>; leafs: Map<string, SubscribeNode> };

     

    简单使用

    // 在任何地方取值及设置数据变化时的监听
    const name = Bi.broker.read("student.teachers[1].name","防止重复订阅 student.teachers[1].name 的 key", ()=>console.log("教师1的姓名改变了"));
    // 在组件中订阅可简写为以下形式
    const name = Bi.broker.read("student.teachers[1].name");
    // 在组件中订阅的简写等价于
    const name = Bi.broker.read("student.teachers[1].name",this,()=>this.elmtRefresh());
    
    // 在任何地方更新值,都将触发如组件刷新等监听
    Bi.broker.write("student.teachers[1].name","教师1的新名字");

    // 不考虑 teacher 还有其它属性,则也等价于这么写
    // 不同之处在于触发监听的范围 student.teachers[1].name 和 student.teachers[1]
    // 很容易理解,后者触发范围更广。student.teachers[1] 会触发 students.teachers[1].**.* 的订阅

    Bi.broker.write("student.teachers[1]",{ name: "教师1的新名字" });

    注:

    Bi 是行为模块的 IOC 控制反转容器,意为 Behaviors IOC container

    符合开发规范的模块会在运行时动态初始化并注入到 Bi 中,使用 『Bi.类名首字母小写』 的形式进行调用。

    关于 IOC 这一老生常谈的东西这里不多做赘述,具体模块工厂的实现可了解上文发的 Gitee 开源项目链接

    总结:

    最终做到非侵入式、更加通用的状态共享

    摒弃了 react-redux 侵入式、和 redux 适用性更差使用更复杂而没有必要的开发模式

    性能还存在优化的空间( 如延迟合并触发、调用更新但值未变等)

    内存占用方面有一定自动清理手段,在可控范围内不至于订阅对象在 Map 缓存中无限膨胀,但仍存在可优化空间

    欢迎提出宝贵意见

     

     

  • 相关阅读:
    nsmutableset
    数组建立 不可变数组 排序 遍历
    字符串截取 拼接 转换 长度 查询 比较
    字典排序
    数字字典结合
    可变字典
    字典
    可变字符串
    oc block排序
    oc中文首字母排序
  • 原文地址:https://www.cnblogs.com/harry-lien/p/14687731.html
Copyright © 2011-2022 走看看