# ArkCombine **Repository Path**: fireice2048/ark-combine ## Basic Information - **Project Name**: ArkCombine - **Description**: 参考 RAC 以及 Swift Combine 的通信框架接口,实现的一套鸿蒙Ark版轻量级通信工具。 Ark 的 Promise 很好用,但是它是一次性的,当需要多次监听一个信号的时候,Promise 就无法很好处理了,ArkCombine 则是为解决这一问题而存在的。 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-08-18 - **Last Updated**: 2024-10-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ArkCombine ## 介绍 **ArkCombine**是参考 RAC 以及 Swift Combine 的通信框架接口,实现的一套**鸿蒙Ark版轻量级通信工具**。 Ark 的 Promise 很好用,但是它是一次性的,当需要多次监听一个信号的时候,Promise 就无法很好处理了,ArkCombine 正是为解决这一问题而存在的。 可以直接将特有事件信号发送出去,也可以让订阅者监听属性变量的改变。 支持一对多通信,不支持事务流,事务流可以直接使用 Ark 自带的 Promise。 ## 软件架构 软件架构说明 **ACSubject** -- 可以被订阅的主题,持有多个ACPublisher,简单发送信号,不会保留数据 **ACPublisher** -- 处理信号的发送 **ACSubscriber** -- 订阅器对象,持有订阅回调 Block **ACSubscriberHolder** -- 订阅器持有,用于持有订阅器对象 **ACSubscriberHolderSet** -- 订阅器持有Set,用于持有多个 ACSubscriberHolder **ACCurrentValueSubject** -- 持有一个当前值,在订阅发生的瞬间,立即将当前保存的 Value 发送给订阅者 **ACState** -- 用于建立状态属性的监听 **ACPublished** -- 用于声明可监听状态变量的注解 ## 安装教程 1. 安装 ohpm install @actools/arkcombine 仓库地址:https://gitee.com/fireice2048/ark-combine 仓库地址:https://atomgit.com/fireice2048/ArkCombine 2. 添加依赖项 ArkCombine oh-package.json5 ``` typescript "dependencies": { "@actools/arkcombine": "1.2.0" } ``` 3. 引入 ArkCombine ``` typescript import { ACSubject, ACSubscriber, ACSubscriberHolderSet } from '@actools/arkcombine'; ``` ## 使用说明 ### ACSubject 使用示例 以登录流程为例,来举例说明 ACSubject / ACSubscriber/ ACSubscriberHolderSet 的用法 1. 首先假定已经存在一个 LoginModel 实现代码(这段代码跟ArkCombine无关,可忽略): ``` typescript export interface UserInfo { userId?: number; nickName?: string; mobile?: string; title?: string; } export class LoginModel { loginRequest(): Promise { return new Promise((resolve, reject) => { setTimeout(() => { let randomInt = this.getRandomInt(0, 2); if (0 == randomInt) { reject(new Error('网络不给力,登录失败')); } else { resolve({ userId: 17915, nickName: '晴天' }); } }, this.getRandomInt(3, 5) * 1000); }); } getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } } ``` LoginModel 实现登录请求发起以及结果返回,这里只是演示,直接在 3~5 秒内返回登录成功或失败 2. LoginController 对 LoginModel 进行封装,封装了整个登录流程以及定义了登录状态枚举 ``` typescript import { ACSubject } from '@actools/arkcombine'; import { LoginModel } from './LoginModel'; export enum LoginState { None = 0, Logging = 1, Logged = 2, Logfailed = 3, } export class LoginController { loginState: LoginState = LoginState.None; loginStateSubject: ACSubject = new ACSubject(); startLogin() { console.log('登录开始...'); this.loginState = LoginState.Logging; this.loginStateSubject.sendValue(this.loginState); let loginModel = new LoginModel(); loginModel.loginRequest().then((userInfo) => { console.log('登录成功 userId:', userInfo.userId, 'nickName:', userInfo.nickName, 'mobile:', userInfo.mobile); this.loginState = LoginState.Logged; this.loginStateSubject.sendValue(this.loginState); }).catch((error: Error) => { console.log('登录失败:', error.message); this.loginState = LoginState.Logfailed; this.loginStateSubject.sendValue(this.loginState); }); } } ``` 注意代码中定义了一个成员:loginStateSubject: ACSubject< LoginState >,用于被订阅的信号主题。 当登录状态发生变化时,则调用 loginStateSubject.sendValue(LoginState) 来发送信号。 3. 订阅器创建,信号接收 ``` typescript import { ACSubscriberHolderSet } from '@actools/arkcombine'; import { LoginController, LoginState } from '../login/LoginController'; @Entry @Component struct Index { @State message: string = 'Hello World'; loginController: LoginController = new LoginController(); private subscribeHolderSet: ACSubscriberHolderSet = new ACSubscriberHolderSet(); aboutToAppear(): void { let holder = this.loginController.loginStateSubject.addPublisher().subscribeValue((state: LoginState) => { console.log('[监听者1号]监听到登录状态变化 state:', LoginState[state]); holder.cancelSubscribe(); }).store(this.subscribeHolderSet); this.loginController.loginStateSubject.addPublisher().subscribeValue((state: LoginState) => { console.log('[监听者2号]监听到登录状态变化 state:', LoginState[state]); }).store(this.subscribeHolderSet); this.loginController.startLogin(); this.loginController.loginStateSubject.addPublisher().subscribeValue((state: LoginState) => { console.log('[监听者3号]监听到登录状态变化 state:', LoginState[state]); }).store(this.subscribeHolderSet);; } build() { } } ``` 4. 输出结果和代码分析 控制台将输出以下日志: ``` 登录开始... [监听者1号]监听到登录状态变化 state: Logging [监听者2号]监听到登录状态变化 state: Logging 大约5秒后输出: 登录成功 userId: 17915 nickName: 晴天 mobile: undefined [监听者2号]监听到登录状态变化 state: Logged [监听者3号]监听到登录状态变化 state: Logged ``` 可以看到,由于一号监听者在收到第一次订阅通知时,就调用了 holder.cancelSubscribe(); 取消了订阅,所以只收到一次通知,后续不再收到回调。 3号监听者由于是登录已经开始后,才建立的监听订阅,所以只收到一次登录成功通知。 另外注意以上代码,.store(this.subscribeHolderSet); 是必需的,否则订阅器不会收到回调,原因是: Ark 的 Block 是持有 this 对象的(无论是否使用 this),这就导致 Publisher 必需持有 Subscriber,不然 Block 可能会被销毁而收不到事件回调,不符合开发者预期。而如果使用者监听了一个全局单例对象的信号,那么这些订阅器就永远得不到销毁,订阅器 Block 持有的 this 对象也就无法销毁,从而产生内存泄漏。ArkCombine 的解决方案是:让使用者必需创建一个 ACSubscribeHolderSet 对象,然后将订阅产生的订阅器 Holder,通过 store 方法传递给 subscribeHolderSet 来持有,当需要解除订阅的时候,调用一下 ACSubscribeHolderSet.cancelAll 来取消订阅,那么即使不显式调用 cancelAll,由于 Publisher 未持有订阅器,哪怕 this 对象跟订阅器互相持有,真正垃圾回收的时候,this对象和订阅器对象也是能得到销毁的,从而避免了内存泄漏。 所以 **ArkCombine 要求所有的订阅器都必需将 subscribeValue 产生的 subscribeHolder,store 到一个 subscribeHolderSet,才能正常收到回调通知。** 如果我们在 startLogin 前添加一句 cancelAll(): ``` this.subscribeHolderSet.cancelAll(); this.loginController.startLogin(); ``` 控制台日志将如下: ``` 登录开始... 登录成功 userId: 17915 nickName: 晴天 mobile: undefined [监听者3号]监听到登录状态变化 state: Logged ``` 可以看到,只剩下3号监听者收到了一次订阅通知回调,因为前面的1号2号监听者在登录发起前已经被取消了,就不会再收到任何回调了。 以上测试代码,可以在项目 entry 里面找到。 ### ACCurrentValueSubject 使用示例 直接看代码: ``` typescript import { ACCurrentValueSubject } from '@actools/arkcombine'; // 车辆行驶方向 enum DrivingDirection { East = 0, South = 1, West = 2, North = 3, } export class CurrentValueTest { direction: DrivingDirection = DrivingDirection.North; directionChangedSubject: ACCurrentValueSubject = new ACCurrentValueSubject(this.direction); setDrivingDirection(direction: DrivingDirection) { this.direction = direction; this.directionChangedSubject.sendValue(this.direction); } startTest() { this.directionChangedSubject.addPublisher().subscribeValue((direction) => { console.log('CurrentValueTest [监听者A]监听到行驶方向变化 direction:', DrivingDirection[direction]); }); } } ``` 控制台输出日志: ``` typescript CurrentValueTest [监听者A]监听到行驶方向变化 direction: North ``` 可以看到,当建立监听的时候,订阅器立即收到了回调,参数 direction 为当前方向 value: 北向。 接下来,我们将代码做简单修改,在建立监听后,设置行驶方向为向南。 ``` typescript startTest() { this.directionChangedSubject.addPublisher().subscribeValue((direction) => { console.log('CurrentValueTest [监听者A]监听到行驶方向变化 direction:', DrivingDirection[direction]); }); this.setDrivingDirection(DrivingDirection.South); } ``` 控制台打印: CurrentValueTest [监听者A]监听到行驶方向变化 direction: North **ArkCombine ACSubscriber 没有添加到 ACSubscriberHolderSet, 无法收到订阅通知** 说明当订阅器没有添加到 ACSubscriberHolderSet 时,ACCurrentValueSubject的订阅器是可以收到当前值通知回调的,但后续不再接收任何通知。 接下来,我们再将代码做简单修改,新增监听者B,并在其 addPublisher() 后添加 .dropFirst(),然后两个监听者都使用上 ACSubscriberHolderSet: ``` typescript private subscribeHolderSet: ACSubscriberHolderSet = new ACSubscriberHolderSet(); startTest() { this.directionChangedSubject.addPublisher().subscribeValue((direction) => { console.log('CurrentValueTest [监听者A]监听到行驶方向变化 direction:', DrivingDirection[direction]); }).store(this.subscribeHolderSet); this.directionChangedSubject.addPublisher().dropFirst().subscribeValue((direction) => { console.log('CurrentValueTest [监听者B]监听到行驶方向变化 direction:', DrivingDirection[direction]); }).store(this.subscribeHolderSet); this.setDrivingDirection(DrivingDirection.South); } ``` 控制台打印: ``` typescript CurrentValueTest [监听者A]监听到行驶方向变化 direction: North CurrentValueTest [监听者A]监听到行驶方向变化 direction: South CurrentValueTest [监听者B]监听到行驶方向变化 direction: South ``` 使用 subscribeHolderSet 后,两个监听者都能正常监听,使用 .dropFirst() 的监听者不再收到当前值通知消息。 ### ACState 使用示例 ACState 是用来监听属性变化的,使用时,将需要开放监听的属性成员使用 @ACPublished 注解来声明,看如下示例代码: 1. 使用 @ACPublished 定义属性 ``` typescript // 车辆行驶方向 enum DrivingDirection { East = 0, South = 1, West = 2, North = 3, } class CurrentValueTest { @ACPublished direction: DrivingDirection = DrivingDirection.North; private subscribeHolderSet: ACSubscriberHolderSet = new ACSubscriberHolderSet(); } ``` 2. 使用 ACState 建立监听 ``` typescript startTest() { console.log('属性监听 开始 direction:', DrivingDirection[this.direction]); ACState.addObserver(this, 'direction')?.subscribeValue((value) => { console.log('[3号]属性监听 direction:', DrivingDirection[value]); }).store(this.subscribeHolderSet); ACState.addObserver(this, 'direction')?.dropFirst().subscribeValue((value) => { console.log('[4号]属性监听 direction:', DrivingDirection[value]); }).store(this.subscribeHolderSet); this.setDrivingDirection(DrivingDirection.South); this.setDrivingDirection(DrivingDirection.West); } ``` 3. 运行程序,打印结果: ``` typescript 属性监听 direction 开始的初值: North [3号]属性监听 direction: North [3号]属性监听 direction: South [4号]属性监听 direction: South [3号]属性监听 direction: West [4号]属性监听 direction: West ``` 可以看到,3号被回调了初始值 North,而 4号没有被回调初始值 North,因为 4号监听者使用了 dropFirst() 丢弃了当前值通知。 ## 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request ## 特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)