可观察对象的冷热转换:深入理解与实践
1. 多播与单播:冷热可观察对象的基础
在计算机网络中,多播(Multicast)是一种一对多的通信形式,信息从单个源发送到多个目的地;而单播(Unicast)则是向单个目的地地址发送专用消息。在可观察对象的世界里,热可观察对象采用多播模式,冷可观察对象则使用单播模式。
从函数式编程(FP)的角度来看,若可观察对象直接访问外部数据,这就产生了副作用,并非纯函数。但在实际应用中,这样做的好处是生产者能被所有订阅者共享,并向他们发送数据,形成多播模式。
2. 使热可观察对象变冷
在获取远程数据时,我们常使用 Promise。然而,Promise 是热的,其发出的值会在所有订阅者之间共享,且不可重复或重试。那么如何用它来获取新的股票报价呢?这与可观察对象的实例化位置有关。
如果全局(急切地)执行 Promise 请求,相同的值(或错误)会广播给所有订阅者,示例代码如下:
const futureVal = new Promise((resolve, reject) => {
const value = computeValue();
resolve(value);
});
const promise$ = Rx.Observable.fromPromise(futureVal);
promise$.subscribe(console.log);
promise$.subscribe(console.log);
要使这个可观察对象变冷,可以通过
ajax()
将 Promise 的实例化移到可观察对象的上下文中,示例代码如下:
const requestQuote$ = symbol =>
Rx.Observable.fromPromise(ajax(...))
...
const fetchDataInterval$ = symbol => twoSecond$
.mergeMap(() => requestQuote$(symbol)
...
本质上,这类似于将事件的源或生产者移到可观察对象的上下文中,示例代码如下:
const coldPromise$ = new Rx.Observable(observer => {
const futureVal = new Promise((resolve, reject) => {
const value = computeValue();
resolve(value);
});
futureVal.then(result => {
observer.next(result);
observer.complete();
});
});
coldPromise$.subscribe(console.log);
coldPromise$.subscribe(console.log);
同样的原则也适用于 WebSockets。如果想为每个订阅者建立专用连接,可以将生产者的激活封装在可观察对象的上下文中,示例代码如下:
const ws$ = new Rx.Observable(observer => {
const socket = new WebSocket('ws://localhost:1337');
socket.addEventListener('message', e => observer.next(e));
return () => socket.close();
});
const sub1 = ws$.map(msg => JSON.parse(msg.data))
.subscribe(msg => console.log(`Sub1 ${msg}`));
const sub2 = ws$.map(msg => JSON.parse(msg.data))
.subscribe(msg => console.log(`Sub2 ${msg}`));
3. 使冷可观察对象变热
以股票行情小部件为例,若要将冷可观察对象变为热可观察对象,需要将事件源(股票行情)从可观察对象管道中移开。因为多个订阅者各自使用独立的间隔流会导致资源浪费,每个订阅者都会多次建立相同的远程连接来获取数据。
为了优化这种情况,可以使用事件发射器和内部轮询机制,结合
Rx.Observable.fromEvent()
可观察对象,将股票数据推送给所有订阅者。示例代码如下:
class StockTicker extends EventEmitter {
constructor(symbol) {
super();
this.symbol = symbol;
this.intId = 0;
}
tick(symbol, price) {
this.emit('tick', symbol, price);
}
start() {
this.intId = setInterval(() => {
const webservice =
`http://finance.yahoo.com/d/quotes.csv?s=${this.symbol}&f=sa&e=.csv`;
ajax(webservice).then(csv).then(
([symbol, price]) => {
this.tick(symbol.replace(/\"/g, ''), price);
});
}, 2000);
}
stop() {
clearInterval(this.intId);
}
}
const ticker = new StockTicker('FB');
ticker.start();
const tick$ = Rx.Observable.fromEvent(ticker, 'tick',
(symbol, price) => ({'symbol': symbol, 'price': price}))
.catch(Rx.Observable.throw(new Error('Stock ticker exception')));
const sub1 = ticks$.subscribe(
quoteDetails => updatePanel1(quoteDetails.symbol, quoteDetails.price)
);
const sub2 = ticks$.subscribe(
quoteDetails => updatePanel2(quoteDetails.symbol, quoteDetails.price)
);
这样,无论是否有订阅者,事件发射器都会继续获取和发送价格行情,实现了订阅与事件源激活的解耦。
4. 通过操作符创建热流
RxJS 提供了一个方便的操作符
share()
,可以将冷流转换为热流。它能在多个订阅者之间共享对一个流的单个订阅,类似于过去的 DIRECTV,单个卫星信号可以驱动同一房屋内的多台电视。
示例代码如下:
const source$ = Rx.Observable.interval(1000)
.take(10)
.do(num => {
console.log(`Running some code with ${num}`);
});
const shared$ = source$.share();
shared$.subscribe(createObserver('SourceA'));
shared$.subscribe(createObserver('SourceB'));
function createObserver(tag) {
return {
next: x => {
console.log(`Next: ${tag} ${x}`);
},
error: err => {
console.log(`Error: ${err}`);
},
complete: () => {
console.log('Completed');
}
};
}
一旦
share()
前面的可观察对象被订阅,它就变成了热可观察对象,这被称为通过操作符实现的热流。将
share()
操作符应用于股票行情流,可以避免每个订阅者进行不必要的 HTTP 调用,示例代码如下:
const ticks$ = symbol$.mergeMap(fetchDataInterval$).share();
const sub1 = ticks$.subscribe(
quoteDetails => updatePanel1(quoteDetails.symbol, quoteDetails.price)
);
const sub1 = ticks$.subscribe(
quoteDetails => updatePanel2(quoteDetails.symbol, quoteDetails.price)
);
5. 同步事件源共享的陷阱
share()
操作符在许多情况下很有用,但在处理同步事件源时需要注意。例如:
const source$ = Rx.Observable.from([1,2,3,4])
.filter(isEven)
.map(x => x * x)
.share();
source$.subscribe(x => console.log(`Stream 1 ${x}`));
source$.subscribe(x => console.log(`Stream 2 ${x}`));
这段代码看似可以让两个观察者共享结果,但实际上只有
Stream 1
会执行。原因有两个:一是调度问题,订阅同步源(如数组)会在第二个
subscribe
语句执行之前完成;二是
share()
引入了状态,第一个订阅会使可观察对象开始发出值,只要至少有一个订阅者继续监听,它就会继续发出值,直到源完成。如果不小心,这种行为可能会导致微妙的错误。
6. 完整的股票行情小部件代码
以下是一个完整的股票行情小部件代码,包含了价格和当日变化的跟踪功能:
const csv = str => str.split(/,\s*/);
const cleanStr = str => str.replace(/\"|\s*/g, '');
const webservice = 'http://download.finance.yahoo.com/d/quotes.csv?s=$symbol&f=$options&e=.csv';
const requestQuote$ = (symbol, opts = 'sa') =>
Rx.Observable.fromPromise(
ajax(webservice.replace(/\$symbol/,symbol)
.replace(/\$options/, opts)))
.retry(3)
.catch(err => Rx.Observable.throw(
new Error('Stock data not available. Try again later!')))
.map(cleanStr)
.map(data => data.indexOf(',') > 0 ? csv(data) : data);
const twoSecond$ = Rx.Observable.interval(2000);
const fetchDataInterval$ = symbol => twoSecond$
.mergeMap(() => requestQuote$(symbol)
.distinctUntilChanged((previous, next) => {
let prevPrice = parseFloat(previous[1]).toFixed(2);
let nextPrice = parseFloat(next[1]).toFixed(2);
return prevPrice === nextPrice;
}));
const symbol$ = Rx.Observable.of('FB', 'CTXS', 'AAPL');
const ticks$ = symbol$.mergeMap(fetchDataInterval$).share();
ticks$.subscribe(
([symbol, price]) => {
let id = 'row-' + symbol.toLowerCase();
let row = document.querySelector(`#${id}`);
if(!row) {
addRow(id, symbol, price);
}
else {
updateRow(row, symbol, price);
}
},
error => console.log(error.message));
ticks$
.mergeMap(([symbol, price]) =>
Rx.Observable.of([symbol, price])
.combineLatest(requestQuote$(symbol, 'o')))
.map(R.flatten)
.map(([symbol, current, open]) => [symbol, (current - open).toFixed(2)])
.do(console.log)
.subscribe(([symbol, change]) => {
let id = 'row-' + symbol.toLowerCase();
let row = document.querySelector(`#${id}`);
if(row) {
updatePriceChange(row, change);
}
},
error => console.log(`Fetch error occurred: ${error}`)
);
这个代码综合了之前学习的多种操作符,实现了股票行情的实时更新和价格变化的计算。
综上所述,通过理解和掌握可观察对象的冷热转换,我们可以更高效地处理异步数据,避免资源浪费,提高应用程序的性能和可维护性。在实际应用中,需要根据具体需求选择合适的转换方法,并注意同步事件源共享可能带来的问题。
可观察对象的冷热转换:深入理解与实践
7. 冷热转换操作的总结与对比
为了更清晰地理解冷热可观察对象的转换操作,下面通过表格进行总结对比:
| 转换类型 | 操作方法 | 示例代码 | 特点 |
| — | — | — | — |
| 热转冷 | 将 Promise 或 WebSocket 等生产者的实例化移到可观察对象上下文中 |
javascript<br>const requestQuote$ = symbol => <br> Rx.Observable.fromPromise(ajax(...))<br>...<br>const fetchDataInterval$ = symbol => twoSecond$<br>.mergeMap(() => requestQuote$(symbol)<br> ...<br>
| 每个订阅者有独立的生产者实例,避免数据共享 |
| 冷转热 | 使用事件发射器结合
Rx.Observable.fromEvent()
或
share()
操作符 |
javascript<br>class StockTicker extends EventEmitter {<br> constructor(symbol) {<br> super();<br> this.symbol = symbol;<br> this.intId = 0;<br> }<br> tick(symbol, price) {<br> this.emit('tick', symbol, price); <br> }<br> start() {<br> this.intId = setInterval(() => {<br> const webservice =<br>`http://finance.yahoo.com/d/quotes.csv?s=${this.symbol}&f=sa&e=.csv`;<br> ajax(webservice).then(csv).then(<br> ([symbol, price]) => {<br> this.tick(symbol.replace(/\"/g, ''), price); <br> }); <br> }, 2000);<br> } <br> stop() {<br> clearInterval(this.intId);<br> }<br>}<br>const ticker = new StockTicker('FB');<br>ticker.start();<br>const tick$ = Rx.Observable.fromEvent(ticker, 'tick', <br> (symbol, price) => ({'symbol': symbol, 'price': price}))<br> .catch(Rx.Observable.throw(new Error('Stock ticker exception')));<br>const sub1 = ticks$.subscribe( <br> quoteDetails => updatePanel1(quoteDetails.symbol, quoteDetails.price) <br>);<br>const sub2 = ticks$.subscribe(<br> quoteDetails => updatePanel2(quoteDetails.symbol, quoteDetails.price) <br>);<br>
或
javascript<br>const source$ = Rx.Observable.interval(1000)<br> .take(10)<br> .do(num => {<br> console.log(`Running some code with ${num}`);<br> });<br>const shared$ = source$.share();<br>shared$.subscribe(createObserver('SourceA'));<br>shared$.subscribe(createObserver('SourceB'));<br>
| 多个订阅者共享生产者实例,减少资源重复使用 |
8. 操作步骤梳理
在实际应用中,进行冷热可观察对象转换的操作步骤如下:
1.
热转冷操作步骤
:
- 确定需要转换的热可观察对象,如基于 Promise 或 WebSocket 的可观察对象。
- 将生产者(Promise 或 WebSocket)的实例化代码移到可观察对象的创建函数内部。
- 重新创建可观察对象,确保每个订阅者都有独立的生产者实例。
2.
冷转热操作步骤
:
- 对于使用事件驱动的场景,创建事件发射器类,在类中实现事件的触发和轮询机制。
- 使用
Rx.Observable.fromEvent()
将事件发射器的事件转换为可观察对象。
- 对于普通的可观察对象,使用
share()
操作符将其转换为热可观察对象。
9. 流程图展示
下面是一个简单的 mermaid 流程图,展示了冷热可观察对象转换的决策过程:
graph TD;
A[判断可观察对象类型] --> B{冷还是热};
B -->|冷| C[是否需要共享资源];
C -->|是| D[使用 share() 或事件发射器];
C -->|否| E[保持冷状态];
B -->|热| F[是否需要独立实例];
F -->|是| G[将生产者实例化移到可观察对象内部];
F -->|否| H[保持热状态];
10. 实际应用中的注意事项
在实际应用可观察对象的冷热转换时,还需要注意以下几点:
-
资源管理
:冷转热时,虽然共享资源可以减少开销,但要确保在所有订阅者取消订阅后,资源能正确释放,避免内存泄漏。例如,在使用事件发射器时,要在合适的时机停止轮询。
-
错误处理
:无论是热转冷还是冷转热,都要做好错误处理。例如,在使用
Rx.Observable.fromPromise()
时,要使用
catch
操作符捕获可能的错误,并进行相应的处理。
-
性能优化
:在处理大量数据或高频率事件时,要注意性能优化。例如,在热转冷时,避免频繁创建和销毁生产者实例;在冷转热时,确保事件发射器的轮询间隔合理。
11. 总结与展望
可观察对象的冷热转换是处理异步数据的重要技术,通过合理运用这些转换方法,可以提高应用程序的性能和可维护性。在实际开发中,要根据具体需求选择合适的转换方式,并注意同步事件源共享等可能出现的问题。
随着前端技术的不断发展,可观察对象在更多场景中得到应用,如实时数据处理、响应式编程等。未来,我们可以期待更多关于可观察对象的优化和创新,为开发者提供更强大、更便捷的工具。同时,开发者也需要不断学习和实践,深入理解可观察对象的原理和应用,以应对日益复杂的开发需求。
超级会员免费看
43

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



