26、RxJS 中的多播操作符与测试基础

RxJS 中的多播操作符与测试基础

1. 连接一个可观察对象到多个观察者

在 RxJS 和网络世界中,单点到单点的传输称为单播,而一对多的传输称为多播。 share() 方法是一个实用且强大的快捷方式,可使用相对较少的代码实现复杂示例。不过,要深入理解其底层原理,需要探索 ConnectableObservable 这种可观察对象。

const updatePriceChange = (rowElem, change) => { 
    let [,, changeElem] = rowElem.childNodes;  
    let priceClass = "green-text", priceIcon="up-green"; 
    if(parseFloat(change) < 0) { 
        priceClass = "red-text"; 
        priceIcon="down-red"; 
    }   
    changeElem.innerHTML = 
    `<span class="${priceClass}">
        <span class="${priceIcon}">
            (${parseFloat(Math.abs(change)).toFixed(2)})
        </span>
    </span>`;   
};

多播操作符有多种特化形式,下面介绍几种常见的:
- Publish publish() 是基本的多播特化操作符,它创建一个可观察对象,允许将单个订阅分发给多个订阅者。与 share() 不同, share() 会根据订阅者数量自动管理源流的订阅和取消订阅,而 publish() 更底层,需要显式调用 connect() 方法来启动源可观察对象。

const source$ = Rx.Observable.interval(1000)
  .take(10)
  .do(num => {
    console.log(`Running some code with ${num}`);
  });
const published$ = source$.publish();
published$.subscribe(createObserver('SubA'));
published$.subscribe(createObserver('SubB'));
published$.connect();

需要注意, connect() 是一个底层操作符,需要手动确保在某个时刻取消订阅,否则可能会导致内存泄漏。

ConnectableObservable 的接口大致如下:

interface ConnectableObservable<T> extends Observable<T> {
  connect() : Subscription
  refCount(): Observable<T>  
}

这里有两个重要概念:
- connect() 方法返回一个 Subscription 实例,代表共享的底层订阅,取消该订阅会使所有订阅者不再接收事件。
- refCount() 方法基于引用计数的概念,返回一个可观察序列,只要有至少一个活动订阅,就会保持与源的连接。 share() 操作符实际上就是 publish().refCount() 的别名。

以下是 publish() 操作的流程图:

graph LR
    A[Source Observable] -->|publish()| B[ConnectableObservable]
    B -->|subscribe()| C[Subscriber A]
    B -->|subscribe()| D[Subscriber B]
    B -->|connect()| E(Start emitting events)
    E --> C
    E --> D
  • Publish with replay publishReplay() 用于将最近的 1 个、10 个、100 个或所有值分发给所有订阅者。该操作符使用多个参数来确定要维护的缓冲区的特性。
const source$ = Rx.Observable.interval(1000)
  .take(10)
  .do(num => {
    console.log(`Running some code with ${num}`);
  });
const published$ = source$.publishReplay(2);
published$.subscribe(createObserver('SubA'));
setTimeout(() => {
  published$.subscribe(createObserver('SubB'));
}, 5000)
published$.connect();

运行上述代码,当第二个订阅者加入时,会先收到流中的最后两个事件(当前和上一个),之后两个订阅者将接收相同的事件。

  • Publish last publishLast() 返回一个可连接的可观察序列,它共享一个仅包含最后一个通知的订阅,将序列中的最后一个可观察值多播给所有订阅者。
const published$ = source$.publishLast();
published$.subscribe(createObserver('SubA'));
published$.subscribe(createObserver('SubB'));  
published$.connect();
2. 可观察对象的冷热特性
  • 冷可观察对象是被动的,它会等待订阅者监听才为每个订阅者执行单独的管道,管理事件生产者的生命周期。
  • 热可观察对象是主动的,无论是否有订阅者监听,都可以开始发射事件,其生命周期独立于源。例如 WebSockets 和 DOM 元素等事件发射器就是热可观察对象的例子。
  • 热可观察对象的事件如果没有人监听就会丢失,而冷可观察对象每次订阅时都会重新构建其管道。
3. 测试的重要性与功能编程的可测试性

在软件开发中,测试是必不可少的。它不仅能帮助捕获编程错误和发现代码的脆弱点,还能确保对需求有统一的理解,记录代码的预期行为。

单元测试用于对单个工作单元(函数)的功能创建期望或断言。功能编程中的纯函数天生比有状态的函数更容易测试,因为纯函数具有明确的输入和可预测的输出(边界条件),并且是确定性的,其结果直接由传入的参数决定。

使用面向对象编程(OOP)编写的应用在进行单元测试时可能会遇到以下常见问题:
- 方法依赖于外部状态,每个测试都需要正确设置和销毁这些状态。
- 方法与系统的其他模块紧密耦合,无法独立测试。
- 应用设计缺乏适当的依赖注入策略,无法正确模拟对第三方依赖的调用。
- 方法长且复杂,包含许多内部逻辑路径,需要编写多个测试来覆盖所有流程。
- 测试运行的顺序可能会影响被测试函数的输出结果。

而纯函数通常范围小,参数清晰,输出可预测,就像一个具有简单边界条件的黑盒。一半的测试工作是准备全面的输入集,另一半是断言返回值仅与被测试函数的逻辑匹配,不受外部因素影响。

4. 使用 Mocha.js 进行测试

我们使用 Mocha.js 作为单元测试框架,它支持多种断言库。在使用时,需要指定测试的 UI 风格,这里使用 BDD UI。

// 浏览器中设置
mocha.setup({ ui: 'bdd', checkLeaks: true});
// 服务器上运行
mocha --check-leaks –-ui tests.js

Mocha 还具有检测全局变量泄漏的功能。例如,下面的函数存在全局变量泄漏问题:

function average(arr) {
    let len = arr.length;
    total = arr.reduce((a, b) => a + b);
    return Math.floor(total / len);
}

为了测试一个纯函数,我们以验证搜索字段输入是否为空的函数为例:

const notEmpty = input => !!input && input.trim().length > 0;
const expect = chai.expect;
describe('Validation', function () {
    it('Should validate that a string is not empty', function() {
        expect(notEmpty('some input')).to.be.equal(true);
        expect(notEmpty(' ')).to.be.equal(false);
        expect(notEmpty(null)).to.be.equal(false);
        expect(notEmpty(undefined)).to.be.equal(false);
    });
});

运行测试可以使用以下命令:

mocha.run();    # 浏览器中
mocha --check-leaks –-ui validation.js # 服务器上

通过上述测试可以看到,测试纯函数简单且设置最少。同时,我们还使用了 Chai.js 作为灵活的断言库,它支持多种测试 API,在处理 Promise 相关测试时,Should.js 会很有用。

RxJS 中的多播操作符与测试基础(续)

5. 多播操作符总结

RxJS 中有许多 multicast() 的重载特化操作符,常见的多播操作符总结如下表:
| 操作符 | 功能 | 示例代码 |
| ---- | ---- | ---- |
| publish() | 创建一个可观察对象,允许将单个订阅分发给多个订阅者,需要显式调用 connect() 启动 | const published$ = source$.publish(); published$.connect(); |
| publishReplay(bufferSize, windowTime) | 将最近的 bufferSize 个值分发给所有订阅者, windowTime 可指定时间窗口 | const published$ = source$.publishReplay(2); published$.connect(); |
| publishLast() | 共享一个仅包含最后一个通知的订阅,将序列中的最后一个可观察值多播给所有订阅者 | const published$ = source$.publishLast(); published$.connect(); |

6. 测试异步代码的挑战与解决方案

在 JavaScript 中,由于存在大量的异步进程需要协调,测试变得困难。为了使异步测试更简单,可以使用 RxJS 的基于可观察对象的测试方法,借助 JavaScript 测试框架 Mocha.js 和 RxJS 工具虚拟调度器。

以下是一个简单的异步代码测试示例:

const Rx = require('rxjs');
const { expect } = require('chai');
const { TestScheduler } = require('rxjs/testing');

describe('Async Observable Test', function () {
    let testScheduler;

    beforeEach(() => {
        testScheduler = new TestScheduler((actual, expected) => {
            expect(actual).deep.equal(expected);
        });
    });

    it('should emit values as expected', () => {
        testScheduler.run(({ cold, expectObservable }) => {
            const source$ = cold('--a--b--|', { a: 1, b: 2 });
            const expected = '--a--b--|';
            expectObservable(source$).toBe(expected);
        });
    });
});

上述代码使用了 TestScheduler 来模拟异步操作, cold 方法创建一个冷可观察对象, expectObservable 用于断言可观察对象的输出是否符合预期。

7. RxJS 调度器

RxJS 调度器用于控制可观察对象何时开始执行以及何时发送通知。虽然调度器功能强大,但在 JavaScript 应用中,特别是客户端应用中,使用调度器并不常见,通常仅在 RxJS 操作符自带的调度器不足以满足需求时使用。

常见的 RxJS 调度器有:
- async 调度器:用于异步操作,如定时器和异步事件。
- queue 调度器:用于同步队列操作,确保任务按顺序执行。
- asap 调度器:用于微任务调度,在当前任务完成后尽快执行。

以下是使用 async 调度器的示例:

const Rx = require('rxjs');
const { async } = require('rxjs/scheduler');

const source$ = Rx.Observable.of(1, 2, 3, async);
source$.subscribe(value => console.log(value));
8. 总结与最佳实践

通过前面的介绍,我们了解了 RxJS 中的多播操作符和测试相关知识。以下是一些总结和最佳实践:
- 多播操作符
- 使用 share() 可以使观察者使用相同的底层源流,并在所有订阅者停止监听时断开连接,将冷可观察对象变为热(或至少是温)可观察对象。
- 使用 publish() publishReplay() publishLast() 等操作符可以创建多播可观察对象,但要注意手动管理订阅以避免内存泄漏。
- 测试
- 纯函数更易于测试,应尽量编写纯函数。
- 使用 Mocha.js 和 Chai.js 进行单元测试,利用 Mocha 的全局变量泄漏检测功能。
- 对于异步代码,使用 RxJS 的虚拟调度器进行测试。

在实际开发中,遵循这些最佳实践可以提高代码的可维护性和可测试性。

以下是一个简单的测试流程的 mermaid 流程图:

graph LR
    A[编写测试用例] --> B[设置测试环境]
    B --> C[运行测试]
    C --> D{测试通过?}
    D -- 是 --> E[结束测试]
    D -- 否 --> F[调试代码]
    F --> A

通过上述内容,我们对 RxJS 中的多播操作和测试有了更深入的理解,希望这些知识能帮助你在开发中更好地使用 RxJS 并编写高质量的代码。

"Mstar Bin Tool"是一款专门针对Mstar系列芯片开发的固件处理软件,主要用于智能电视及相关电子设备的系统维护深度定制。该工具包特别标注了"LETV USB SCRIPT"模块,表明其对乐视品牌设备具有兼容性,能够通过USB通信协议执行固件读写操作。作为一款专业的固件编辑器,它允许技术人员对Mstar芯片的底层二进制文件进行解析、修改重构,从而实现系统功能的调整、性能优化或故障修复。 工具包中的核心组件包括固件编译环境、设备通信脚本、操作界面及技术文档等。其中"letv_usb_script"是一套针对乐视设备的自动化操作程序,可指导用户完成固件烧录全过程。而"mstar_bin"模块则专门处理芯片的二进制数据文件,支持固件版本的升级、降级或个性化定制。工具采用7-Zip压缩格式封装,用户需先使用解压软件提取文件内容。 操作前需确认目标设备采用Mstar芯片架构并具备完好的USB接口。建议预先备份设备原始固件作为恢复保障。通过编辑器修改固件参数时,可调整系统配置、增删功能模块或修复已知缺陷。执行刷机操作时需严格遵循脚本指示的步骤顺序,保持设备供电稳定,避免中断导致硬件损坏。该工具适用于具备嵌入式系统知识的开发人员或高级用户,在进行设备定制化开发、系统调试或维护修复时使用。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值