RxJS 中的冷热可观察对象与 WebSocket 应用
1. 冷热可观察对象简介
可观察函数是一种惰性计算,意味着在观察者订阅之前,包含可观察对象声明的整个操作符序列不会开始执行。但在订阅之前发生的事件会怎样呢?比如鼠标移动可观察对象对所有鼠标事件的处理,或者接收到一组消息的 WebSocket 会如何处理这些消息?这些潜在的重要事件会丢失吗?实际上,这些活跃的数据源不会等待订阅者开始监听才开始发出事件,因此了解如何处理这种情况至关重要。
RxJS 将可观察源分为两类:热可观察对象和冷可观察对象。这一分类不仅决定了订阅语义的行为特征,还影响着流的整个生命周期。可观察对象的“温度”也会影响流和生产者是一起管理还是分开管理,这对资源利用有很大影响。
1.1 冷可观察对象
冷可观察对象在观察者订阅之前不会开始发出所有值。它通常用于包装有界数据类型(如数字、数字范围、字符串、数组和 HTTP 请求)以及无界类型(如生成器函数)。这些资源是被动的,因为它们的声明与执行是独立的,这意味着这些可观察对象在创建和执行时是真正惰性的。
每次新的订阅都会创建一个新的独立流,每个订阅者都会从开始独立地接收完全相同的一组事件。可以将创建冷可观察对象看作是创建一个稍后执行的计划或食谱,每次执行都是从头到尾重复进行。只有当你选择开始“烹饪”时,冷可观察对象才会开始发出事件。
从纯函数式编程(FP)的角度来看,冷可观察对象的行为很像函数。函数可以被视为一个惰性或待计算的值,只有在需要时调用才会返回结果。同样,可观察对象在订阅之前不会运行,你可以使用提供的观察者来处理其返回值。
冷可观察对象的声明通常以静态操作符(如
of()
或
from()
)开始,定时可观察对象
interval()
和
timer()
也表现为冷可观察对象。以下是一个简单的示例:
const arr$ = Rx.Observable.from([1,2,3,4,5,6,7,8,9]);
const sub1 = arr$.subscribe(console.log);
// ... 片刻之后 ... //
const sub2 = arr$.subscribe(console.log);
const evenNumbers$ = Rx.Observable.fromArray(numbersArr)
.filter(num => num % 2 === 0);
evenNumbers$.subscribe(num => {
// 在这里使用偶数
});
对于冷可观察对象,所有订阅者无论何时订阅,都会观察到相同的事件。例如
interval()
操作符,每次新的订阅都会创建一个全新的间隔实例。以下代码展示了如何使用同一个可观察对象的两个订阅者分别监听偶数和奇数:
const interval$ = Rx.Observable.interval(500);
const isEven = x => x % 2 === 0;
interval$
.filter(isEven)
.take(5)
.subscribe(x => {
console.log(`Even number found: ${x}`);
});
interval$
.filter(R.compose(R.not, isEven))
.take(5)
.subscribe(x => {
console.log(`Odd number found: ${x}`);
});
可以用以下流程图来展示冷可观察对象的订阅情况:
graph LR
A[冷可观察对象 interval$] --> B[订阅者 1]
A --> C[订阅者 2]
B --> D(接收事件 1,2,3,4,5...)
C --> E(接收事件 1,2,3,4,5...)
冷可观察对象的定义为:当被订阅时,会向任何活跃的订阅者发出整个事件序列。
1.2 热可观察对象
流并不总是在你想要的时候开始,也不能期望总是从每个可观察对象中获取每个事件。有时,通过延迟订阅,你可能会故意避免某些事件,就像隐式的
skip()
操作。
热可观察对象无论是否有订阅者都会产生事件,它们是活跃的。在现实世界中,热可观察对象用于模拟点击、鼠标移动、触摸等事件,或通过事件发射器暴露的任何其他事件。与冷可观察对象不同,热可观察对象的订阅者通常只接收订阅创建后发出的事件。
热可观察对象在没有订阅者时仍然是惰性的,事件只是发出并被忽略。只有当观察者订阅流时,操作符管道才会开始工作,数据才会向下游流动。这种类型的流对许多开发者来说更直观,因为它与他们熟悉的 Promise 和事件发射器的行为非常相似。
从理论上讲,由于热可观察对象发出的数据具有不可预测和不可重复的性质,它们并不完全是纯的。毕竟,对外部刺激(如按钮点击)的反应可以被视为一种依赖于其他资源(如 DOM 或时间)行为的副作用。但从应用和代码的角度来看,所有可观察对象都可以被视为纯的。
与冷可观察对象为每个订阅者创建独立的数据源副本不同,热可观察对象将相同的订阅共享给所有监听它的观察者。因此,热可观察对象在被订阅时,会从订阅点开始发出正在进行的事件序列,而不是从开始。
可观察对象是热还是冷部分取决于它所包装的源的类型。例如,任何鼠标事件处理程序的可观察对象通常是热的,因为它只是对现有
addEventListener()
调用的抽象,其行为取决于系统对鼠标事件的处理。而包装静态数据源(如数组)或使用生成数据(通过生成器函数)的可观察对象通常是冷的,因为它们在没有订阅者监听时不会开始产生值。
以下是热可观察对象订阅情况的流程图:
graph LR
A[热可观察对象 mouseMove$] --> B[订阅者 1]
A --> C[订阅者 2]
B --> D(从订阅点接收事件)
C --> E(从订阅点接收事件)
热可观察对象的定义为:无论是否有订阅者都会产生事件的可观察对象。
一般来说,尽可能使用冷可观察对象更好,因为它们本质上是无状态的,每个订阅都是独立的,从 RxJS 内部角度来看,需要担心的共享状态更少。
2. 新的数据来源:WebSocket
时间在可观察对象中无处不在,特别是在热可观察对象中。数据源开始发出事件的时间与订阅者开始监听的时间之间的差异可能会导致问题。例如,当你在节目进行中切换到喜欢的电视节目时,可能会错过一些开头的情节。同样,在使用 WebSocket 或其他事件发射器的简单消息系统中,错过任何消息都可能对应用程序的正常运行至关重要。如果在关键消息包到达后才订阅热可观察对象,那些指令可能会丢失。
2.1 WebSocket 简介
除了绑定到 DOM 事件和 AJAX 调用外,RxJS 还可以轻松绑定到 WebSocket。WebSocket(WS)是一种异步通信协议,与传统 HTTP 相比,它为客户端到服务器提供了更快、更高效的通信线路。这对于实时聊天、流媒体服务或游戏等高度交互的应用程序非常有用。
WebSocket 运行在 TCP 连接之上,其优势在于可以在保持连接打开的情况下来回传递信息(利用浏览器的多路复用功能和保持活动特性),并且服务器可以在浏览器没有明确请求的情况下向其发送内容。
WebSocket 通信始于握手,这将 HTTP 世界与 WebSocket 连接起来。在这个过程中,会讨论连接和安全的细节,为双方之间的安全、高效通信铺平道路。具体步骤如下:
1. 在双方之间建立套接字连接以进行初始握手。
2. 将通信协议从常规 HTTP 切换或升级到基于套接字的协议。
3. 双向发送消息(全双工)。
4. 由服务器或客户端发起断开连接。
以下是 WebSocket 通信的流程图:
graph LR
A[客户端] --> B[握手]
B --> C[协议升级]
C --> D[双向消息传递]
D --> E[断开连接]
F[服务器] --> B
这个过程的关键是初始握手,它协商升级过程。由于 WebSocket 使用与 HTTP 和 HTTPS 相同的端口(分别为 80 和 443,WebSocket Secure 协议使用 443),通过相同端口路由请求是有利的,因为防火墙通常配置为允许这些信息自由流动。从异步事件消息传递的角度来看,可以将 WebSocket 视为客户端 - 服务器通信的事件发射器。
2.2 Node.js 中的简单 WebSocket 服务器
为了演示,我们将使用 Node.js 编写一个简单的 WebSocket 服务器,但你也可以使用其他喜欢的平台(如 Python、PHP、Java 或任何具有套接字 API 的平台)。服务器将是一个简单的 TCP 应用程序,使用 Node.js WebSocket API 监听端口 1337(任意选择)。WebSocket 与 HTTP 服务器协商,作为发送和接收消息的工具。一旦服务器收到请求,它将回复消息“Hello Socket”。以下是代码示例:
const Rx = require('rxjs/Rx');
const WebSocketServer = require('websocket').server;
const http = require('http');
// ws port
const server = http.createServer();
server.listen(1337);
// create the server
wsServer = new WebSocketServer({
httpServer: server
});
上述代码中,首先导入了 RxJS 核心 API、WebSocket 服务器库和 HTTP 库,然后创建了一个 HTTP 服务器并让其监听 1337 端口,最后创建了 WebSocket 服务器并将其与 HTTP 服务器关联起来。
3. 使用 RxJS 处理 WebSocket 消息流
在了解了 WebSocket 的基本原理和如何创建简单的 WebSocket 服务器后,接下来我们将探讨如何使用 RxJS 来处理 WebSocket 的消息流。这可以帮助我们更好地管理异步消息,避免错过重要信息。
3.1 创建 WebSocket 可观察对象
我们可以将 WebSocket 封装成一个可观察对象,这样就可以利用 RxJS 的操作符来处理消息。以下是一个简单的示例:
const Rx = require('rxjs/Rx');
const WebSocket = require('ws');
function createWebSocketObservable(url) {
return Rx.Observable.create(observer => {
const socket = new WebSocket(url);
socket.on('open', () => {
observer.next('WebSocket 连接已打开');
});
socket.on('message', (message) => {
observer.next(message);
});
socket.on('close', () => {
observer.complete();
});
socket.on('error', (error) => {
observer.error(error);
});
return () => {
if (socket.readyState === WebSocket.OPEN) {
socket.close();
}
};
});
}
const webSocket$ = createWebSocketObservable('ws://localhost:1337');
webSocket$.subscribe(
(message) => console.log('收到消息:', message),
(error) => console.error('发生错误:', error),
() => console.log('WebSocket 连接已关闭')
);
在上述代码中,我们定义了一个
createWebSocketObservable
函数,它接受一个 WebSocket 的 URL 作为参数,并返回一个可观察对象。在可观察对象的创建过程中,我们监听了 WebSocket 的
open
、
message
、
close
和
error
事件,并分别调用观察者的
next
、
complete
和
error
方法。同时,我们还返回了一个清理函数,用于在订阅取消时关闭 WebSocket 连接。
3.2 处理 WebSocket 消息的操作符
有了 WebSocket 可观察对象后,我们可以使用 RxJS 的操作符来处理消息。例如,我们可以使用
map
操作符将接收到的消息进行转换,使用
filter
操作符过滤掉不需要的消息。以下是一个示例:
webSocket$
.map((message) => JSON.parse(message))
.filter((data) => data.type === 'important')
.subscribe(
(data) => console.log('收到重要消息:', data),
(error) => console.error('发生错误:', error),
() => console.log('WebSocket 连接已关闭')
);
在这个示例中,我们使用
map
操作符将接收到的消息解析为 JSON 对象,然后使用
filter
操作符过滤出类型为
important
的消息。最后,我们订阅处理后的可观察对象,只处理重要消息。
3.3 分享 WebSocket 可观察对象
有时候,我们可能需要多个订阅者来处理同一个 WebSocket 连接的消息。为了避免为每个订阅者创建独立的 WebSocket 连接,我们可以使用
share
操作符来分享可观察对象。以下是一个示例:
const sharedWebSocket$ = webSocket$.share();
sharedWebSocket$.subscribe(
(message) => console.log('订阅者 1 收到消息:', message),
(error) => console.error('订阅者 1 发生错误:', error),
() => console.log('订阅者 1: WebSocket 连接已关闭')
);
sharedWebSocket$.subscribe(
(message) => console.log('订阅者 2 收到消息:', message),
(error) => console.error('订阅者 2 发生错误:', error),
() => console.log('订阅者 2: WebSocket 连接已关闭')
);
在这个示例中,我们使用
share
操作符将
webSocket$
可观察对象转换为一个共享的可观察对象
sharedWebSocket$
。这样,多个订阅者就可以共享同一个 WebSocket 连接,避免了资源的浪费。
以下是使用
share
操作符后 WebSocket 可观察对象的订阅情况表格:
| 订阅者 | 接收消息情况 |
| ---- | ---- |
| 订阅者 1 | 共享 WebSocket 连接,接收消息 |
| 订阅者 2 | 共享 WebSocket 连接,接收消息 |
4. 冷热可观察对象与 WebSocket 的综合应用
在实际应用中,我们可能会同时遇到冷热可观察对象和 WebSocket 的情况。以下是一些综合应用的场景和处理方法。
4.1 冷可观察对象与 WebSocket 结合
当我们需要在 WebSocket 连接建立后执行一些初始化操作时,可以使用冷可观察对象。例如,我们可以在 WebSocket 连接打开后,从服务器获取一些初始数据。以下是一个示例:
const initData$ = Rx.Observable.of([1, 2, 3, 4, 5]);
webSocket$.take(1).subscribe(() => {
initData$.subscribe(
(data) => console.log('初始化数据:', data),
(error) => console.error('获取初始化数据时发生错误:', error),
() => console.log('初始化数据获取完成')
);
});
在这个示例中,我们创建了一个冷可观察对象
initData$
,它包含一些初始数据。当 WebSocket 连接打开后,我们订阅
initData$
并获取初始化数据。
4.2 热可观察对象与 WebSocket 结合
WebSocket 本身可以看作是一个热可观察对象,因为它无论是否有订阅者都会接收和发送消息。我们可以利用热可观察对象的特性,让多个订阅者共享 WebSocket 的消息流。例如,我们可以在一个页面上有多个组件都需要接收 WebSocket 的消息,这时就可以使用热可观察对象来实现。
以下是一个简单的流程图,展示了热可观察对象与 WebSocket 结合的应用:
graph LR
A[WebSocket 服务器] --> B[WebSocket 连接]
B --> C[热可观察对象 WebSocket$]
C --> D[订阅者 1]
C --> E[订阅者 2]
D --> F(处理消息)
E --> G(处理消息)
4.3 处理消息丢失问题
在使用热可观察对象的 WebSocket 时,可能会遇到消息丢失的问题。为了避免这种情况,我们可以使用一些 RxJS 的操作符,如
replay
或
shareReplay
。以下是一个使用
shareReplay
的示例:
const replayWebSocket$ = webSocket$.shareReplay(1);
replayWebSocket$.subscribe(
(message) => console.log('订阅者 1 收到消息:', message),
(error) => console.error('订阅者 1 发生错误:', error),
() => console.log('订阅者 1: WebSocket 连接已关闭')
);
setTimeout(() => {
replayWebSocket$.subscribe(
(message) => console.log('订阅者 2 收到消息:', message),
(error) => console.error('订阅者 2 发生错误:', error),
() => console.log('订阅者 2: WebSocket 连接已关闭')
);
}, 5000);
在这个示例中,我们使用
shareReplay(1)
操作符将 WebSocket 可观察对象转换为一个可以重放最后一个消息的共享可观察对象。这样,即使订阅者 2 在订阅时已经错过了一些消息,它仍然可以接收到最后一个消息。
通过以上的综合应用,我们可以更好地利用冷热可观察对象和 WebSocket 的特性,处理各种复杂的异步消息场景。
超级会员免费看
17

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



