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 并编写高质量的代码。
超级会员免费看
44

被折叠的 条评论
为什么被折叠?



