深入理解 RxJS 中可观察对象的冷热特性及副作用影响
1. WebSocket 服务端与客户端的实现
在 RxJS 中,我们可以使用
Rx.Observable.fromEvent
来处理 WebSocket 服务端和客户端的通信。以下是服务端的代码示例:
Rx.Observable.fromEvent(wsServer, 'request')
.map(request => request.accept(null, request.origin))
.subscribe(connection => {
connection.sendUTF(JSON.stringify({ msg:'Hello Socket' }));
});
运行服务端的步骤如下:
1. 确保已经安装了 Node.js。
2. 在命令行中执行
node server.js
启动服务。
客户端代码示例如下:
const websocket = new WebSocket('ws://localhost:1337');
Rx.Observable.fromEvent(websocket, 'message')
.map(msg => JSON.parse(msg.data))
.pluck('msg')
.subscribe(console.log);
这个客户端代码的执行流程如下:
1. 创建一个 WebSocket 连接到
ws://localhost:1337
。
2. 使用
Rx.Observable.fromEvent
监听
message
事件。
3. 将接收到的消息数据解析为 JSON 对象。
4. 提取
msg
属性。
5. 将提取的值打印到控制台。
2. 热可观察对象的数据丢失问题
使用 RxJS 与 WebSocket 创建的是热可观察对象,这意味着订阅时不会重新播放之前发出的所有消息,而是从订阅后开始推送消息。例如:
Rx.Observable.timer(3000)
.mergeMap(() => Rx.Observable.fromEvent(websocket, 'message'))
.map(msg => JSON.parse(msg.data))
.pluck('msg')
.subscribe(console.log);
在这个代码中,我们设置了一个 3 秒的延迟来模拟页面初始化的延迟。如果在这 3 秒内 WebSocket 已经开始发送消息,那么这个可观察对象可能会错过这些消息。这种时间依赖可能会受到多种因素的影响,如应用是否从缓存加载、用户系统的处理资源、网络延迟以及页面上的动画等。
3. 重放(Replay)与重新订阅(Resubscribe)的区别
在 RxJS 中,重放和重新订阅是两个重要的概念:
-
重放(Replay)
:是指将相同的事件序列重新发送给每个订阅者,实际上是重新播放整个序列。例如,从 Promise 创建的可观察对象,通过重试或添加新的订阅者来重放时,已完成的 Promise 不会再次调用,而是直接返回值或错误。但重放可能需要大量内存来存储要重新发送的流内容,因此不建议用于像鼠标点击这样的无限事件发射器。
-
重新订阅(Resubscribe)
:会重新创建相同的管道并重新执行产生事件的代码。如果可观察对象的管道保持纯净,那么可以保证所有订阅者对于相同的输入会收到相同的事件。
以下是一个重放的示例代码:
const p = new Promise((resolve, reject) => {
setTimeout(() =>{
let isAtAfter10pm = moment().hour() >= 20;
if(isAtAfter10pm) {
reject(new Error('Too late!'));
}
else {
resolve('Success!');
}
}, 5000);
});
const promise$ = Rx.Observable.fromPromise(p);
promise$.subscribe(val => console.log(`Sub1 ${val}`));
// ... after 10 pm ...//
promise$.subscribe(val => console.log(`Sub2 ${val}`));
无论订阅时间如何,所有订阅者都会收到相同的值,这是因为 Promise 的特性,第二次订阅时不会再次执行 Promise 的主体代码,这就是重放。
重新订阅的示例代码如下:
const interval$ = Rx.Observable.create(observer => {
let i = 0;
observer.next('Starting interval...');
let intervalId = setInterval(() => {
let isAtAfter10pm = moment().hour() >= 20;
if(isAtAfter10pm) {
clearInterval(intervalId);
observer.complete();
}
observer.next(`Next ${i++}`);
}, 1000);
});
// ... before 10 pm ... //
const sub1 = interval$.subscribe(val => console.log(`Sub1 ${val}`));
// ... after 10 pm ... //
const sub2 = interval$.subscribe(val => console.log(`Sub2 ${val}`));
在这个例子中,重新订阅会重新执行代码逻辑,不同的订阅者根据订阅时间可能会收到不同的事件。如果在 10 点之后订阅,可能不会收到任何事件。
4. 副作用对重放和重新订阅的影响
直接在代码中使用时间是明显的副作用,因为时间是全局且不断变化的。热可观察对象在这方面不如冷可观察对象纯粹,冷可观察对象应该向任何订阅者重新发射(或重放)所有项目。例如,在使用
retry()
操作符时,如果直接在 Promise 上使用,可能会出现意外结果。因为 Promise 是热可观察对象,一旦处于已解决或已拒绝状态,就不会再次执行,所以重试操作是无效的。为了解决这个问题,我们可以将 Promise 的创建包装在另一个可观察对象中,使其可以重试。
5. 可观察对象冷热特性的判断
可观察对象的冷热特性可以根据生产者来判断:
-
冷可观察对象
:当可观察对象创建生产者时,它是冷的。例如:
const cold$ = new Rx.Observable(observer => {
const producer = new Producer();
// ...Observer listens to producer,
// producer pushes events to the observer...
producer.addEventListener('some-event', e => observer.next(e));
return () => producer.dispose();
});
在这个例子中,
producer
是在可观察对象的上下文中创建的,每个订阅者都会获得自己的
producer
副本,这是一种单播(unicast)通信。
-
热可观察对象
:当可观察对象封闭生产者时,它是热的。例如:
const producer = new Producer();
const hot$ = new Rx.Observable(observer => {
// ...Observer listens to producer,
// and pushes events onto it...
producer.addEventListener('some-event', e => observer.next(e));
});
在这个例子中,
producer
是在可观察对象外部创建的,其生命周期独立于可观察对象,多个订阅者可以共享同一个
producer
。
以下是冷热可观察对象的对比表格:
| 特性 | 冷可观察对象 | 热可观察对象 |
| ---- | ---- | ---- |
| 生产者创建位置 | 可观察对象内部 | 可观察对象外部 |
| 订阅者与生产者关系 | 每个订阅者有自己的生产者副本 | 多个订阅者共享同一个生产者 |
| 重放特性 | 可以重放之前的事件 | 一般不重放之前的事件 |
6. 总结
理解 RxJS 中可观察对象的冷热特性以及重放和重新订阅的区别对于编写高效、可靠的代码至关重要。在处理资源昂贵的生产者时,如远程 HTTP 调用或 WebSocket,共享生产者可以节省资源;而在需要独立处理数据时,使用冷可观察对象可以确保每个订阅者有自己的处理流程。同时,要注意副作用对可观察对象行为的影响,避免因时间依赖等问题导致数据丢失或结果不一致。
通过以上的介绍和示例代码,我们可以更好地掌握 RxJS 中可观察对象的特性和使用方法,从而在实际项目中更加灵活地应用。
深入理解 RxJS 中可观察对象的冷热特性及副作用影响
7. 改变可观察对象的冷热特性的实际应用
在实际应用中,我们经常会遇到需要改变可观察对象冷热特性的场景。例如,在处理股票行情数据时,我们希望能够高效地获取和处理数据,同时避免不必要的资源浪费。下面我们通过一个具体的例子来详细说明如何改变可观察对象的冷热特性。
首先,我们定义一个获取股票报价的请求函数:
const requestQuote$ = symbol =>
Rx.Observable.fromPromise(
ajax(webservice.replace(/\$symbol/, symbol))
)
.retry(3)
.map(response => response.replace(/"/g, ''))
.map(csv);
在这个代码中,我们使用
fromPromise
创建了一个可观察对象。但是,由于 Promise 是热可观察对象,直接在 Promise 上使用
retry
操作符是无效的。因为 Promise 一旦处于已解决或已拒绝状态,就不会再次执行。
为了解决这个问题,我们将 Promise 的创建包装在另一个可观察对象中:
const fetchDataInterval$ = symbol => twoSecond$
.mergeMap(() => requestQuote$(symbol))
.retry(3)
.catch(err => Rx.Observable.throw(
new Error('Stock data not available. Try again later!')
));
在这个代码中,我们在
mergeMap
操作符内部创建了一个新的 Promise。
retry
操作符会重新订阅投影的可观察对象,而不是直接订阅
fromPromise
创建的可观察对象。这样,我们就改变了可观察对象的冷热特性,使其表现得像冷可观察对象,从而实现了重试功能。
这个过程的流程图如下:
graph TD;
A[twoSecond$ 流] --> B[mergeMap 操作];
B --> C[requestQuote$(symbol) 可观察对象];
C --> D[retry(3) 重试操作];
D --> E{是否成功};
E -- 是 --> F[输出数据];
E -- 否 --> G[catch 错误处理];
G --> H[抛出错误信息];
8. 冷热可观察对象在不同场景下的选择
在实际开发中,我们需要根据不同的场景选择合适的可观察对象类型。下面我们通过一个表格来对比不同场景下冷热可观察对象的优缺点:
| 场景 | 冷可观察对象 | 热可观察对象 |
| ---- | ---- | ---- |
| 处理独立数据 | 每个订阅者有自己的生产者副本,数据处理相互独立,不会相互影响。例如,处理从生成器函数产生的一组对象时,每个订阅者可以独立处理自己的副本。 | 多个订阅者共享同一个生产者,可能会导致数据处理相互干扰。 |
| 资源共享 | 每个订阅者都需要创建自己的生产者,可能会消耗大量资源。例如,每次订阅都需要发起一个新的 HTTP 请求。 | 多个订阅者可以共享同一个生产者,节省资源。例如,多个订阅者可以共享同一个 WebSocket 连接。 |
| 数据重放 | 可以重放之前的事件,确保每个订阅者都能看到完整的数据序列。 | 一般不重放之前的事件,订阅较晚的订阅者可能会错过部分数据。 |
9. 避免副作用对可观察对象的影响
为了避免副作用对可观察对象的影响,我们可以采取以下几个措施:
1.
减少时间依赖
:尽量避免在代码中直接使用时间作为判断条件,因为时间是全局且不断变化的,容易引入副作用。如果确实需要使用时间,可以考虑使用定时器或其他方式来模拟时间的变化。
2.
封装副作用
:将副作用封装在独立的函数或模块中,避免直接在可观察对象的管道中使用。例如,将与时间相关的操作封装在一个函数中,在需要时调用该函数。
3.
使用纯函数
:在可观察对象的管道中尽量使用纯函数,纯函数不会产生副作用,输入相同的参数会返回相同的结果。这样可以确保可观察对象的行为更加稳定和可预测。
10. 总结与展望
通过本文的介绍,我们深入了解了 RxJS 中可观察对象的冷热特性、重放和重新订阅的区别,以及副作用对它们的影响。在实际开发中,我们应该根据具体的场景选择合适的可观察对象类型,并采取相应的措施来避免副作用的影响。
未来,随着前端技术的不断发展,RxJS 作为一种强大的响应式编程库,将会在更多的领域得到应用。我们可以进一步探索 RxJS 的高级特性,如多播、缓存等,以提高代码的性能和可维护性。同时,我们也可以将 RxJS 与其他前端框架结合使用,构建更加复杂和高效的应用程序。
总之,掌握 RxJS 中可观察对象的特性和使用方法是前端开发者必备的技能之一。希望本文能够帮助你更好地理解和应用 RxJS,在实际项目中发挥更大的作用。
超级会员免费看
7

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



