摒弃 react-redux: 非侵入式状态共享实现

前言

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

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

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

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

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

  ( 为什么不叫『状态共享』模块?因为没使用 reducer + action + payload 的模式而不好意思用 )

  原本打算使用 redux 作为底层实现,但是前面已经说了『十分没有必要』

 

设计和实现

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

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

   (关于『前端模块系统的约定』,即 『逻辑模块(实现类)』 辅以『异步初始化』 +『实例化优先级』 + 『依赖声明』 的动态模块组织方式,对于直接使用 ES6 标准的静态 import 方式来组织模块关系,我觉得可称之为『变革』也不为过,但较之其它语言此法已广泛运用十数年)

  废话不多说,看代码

  

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()); } }
// 实现类的初始化函数,可理解为异步构造函数,IOC 工厂进行加工时会调用(不知道IOC-DI是什么?请自行百度搜索『控制反转,依赖注入』) 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; };
// 声明该模块的初始化顺序,firstOrder 是最小整数代表不考虑依赖的情况下,最先初始化 export const order
= () => consts.firstOrder;


// 一些内置私有常量和私有函数,不用 private 修饰并放在主实现类中的目的是封装性更好,更安全。

// 算法解析:
// 1. node:订阅节点,每一个不同的形如『student.teacher.room.name』的数据路径 key 都指向不同节点
// 2. elmtSet:组件集合,这些组件订阅了节点 node (表达能力有限,略显抽象?)
// 3. funcSet: 函数集合,这些函数订阅了节点 node (表达能力有限,略显抽象?) const addToSetCascade
= (node: SubscribeNode, elmtSet: Set<BaseElmt<any, any>>, funcSet: Set<() => void>) => { addToSet(node, elmtSet, funcSet);
// 递归调用,因为如订阅了 student 节点,相当于订阅了它和它的所有多叉树子节点 如 student.teacher, student.name, student.teacher.name, student.teacher.room.name ...... 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> };

 

 

简单使用

// 两个方法:
// 1. 读取:『常规』read( key:string )
// 2. 读取:『万用』read( key:string,unique:string,callback:()=>void )。
// 2. 写入:write( key:string,value:any )。
//
(高级万用) 1% 的情况:在任何地方取值及设置数据变化时的监听 const name = Bi.broker.read("student.teachers[1].name","防止重复订阅 student.teachers[1].name 的 key", ()=>console.log("教师1的姓名改变了")); // (一般常规)99% 的情况:在组件中订阅可简写为以下形式 const name = Bi.broker.read("student.teachers[1].name"); // 这是因为(常规)情况下,由于组件已『AOP』钩子,所以可以直观地只传第一个参数,省略第二、三个参数 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 缓存中无限膨胀,但仍存在可优化空间

欢迎提出宝贵意见

 

 

posted @ 2021-04-22 04:07  本木大人丿  阅读(177)  评论(0编辑  收藏  举报