25、可观察对象的冷热转换:深入理解与实践

可观察对象的冷热转换:深入理解与实践

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. 总结与展望

可观察对象的冷热转换是处理异步数据的重要技术,通过合理运用这些转换方法,可以提高应用程序的性能和可维护性。在实际开发中,要根据具体需求选择合适的转换方式,并注意同步事件源共享等可能出现的问题。

随着前端技术的不断发展,可观察对象在更多场景中得到应用,如实时数据处理、响应式编程等。未来,我们可以期待更多关于可观察对象的优化和创新,为开发者提供更强大、更便捷的工具。同时,开发者也需要不断学习和实践,深入理解可观察对象的原理和应用,以应对日益复杂的开发需求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值