46、RxJS 基础核心概念及操作符详解

RxJS 基础核心概念及操作符详解

1. RxJS 操作符基础

1.1 操作符组合

操作符是可组合的,能够将它们链接起来,使得可观察对象发出的项在到达观察者之前可以由一系列操作符进行处理。例如:

const beers = [
  {name: "Stella", country: "Belgium", price: 9.50},
  {name: "Sam Adams", country: "USA", price: 8.50},
  {name: "Bud Light", country: "USA", price: 6.50}
];
from(beers)
 .filter(beer => beer.price < 8)
 .map(beer => `${beer.name}: $${beer.price}`)
 .subscribe(
    beer => console.log(beer),
    err => console.error(err)
  );
console.log("This is the last line of the script");

从 RxJS 6 开始,链接操作符的唯一方法是使用 pipe() 方法,将用逗号分隔的操作符作为参数传递给它。

1.2 可管道操作符

1.2.1 点链操作符的问题

在 RxJS 6 之前,可以使用点来链接操作符。但这种方式存在问题,例如导入点链操作符时:

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';

这些操作符会修补 Observable.prototype 的代码,如果后续移除某个操作符但忘记移除对应的导入语句,打包工具在进行树摇时可能会保留未使用的代码。

1.2.2 可管道操作符的优势

RxJS 5.5 引入了可管道操作符,它们是纯函数,不会修补 Observable 。可以使用 ES6 导入语法导入操作符,然后将它们包装在 pipe() 函数中:

import {map, filter} from 'rxjs/operators';
import {from} from 'rxjs';
from(beers)
 .pipe(
    filter(beer => beer.price < 8),
    map(beer => `${beer.name}: $${beer.price}`)
  )
 .subscribe(
    beer => console.log(beer),
    err => console.error(err)
  );

这样,如果移除某个操作符,打包工具的树摇模块可以识别出未使用的导入函数,相应操作符的代码将不会包含在打包文件中。

1.3 异步可观察对象

默认情况下, from() 函数返回同步可观察对象。如果需要异步可观察对象,可以使用第二个参数指定异步调度器:

from(beers, Scheduler.async)

这样修改后,会先打印 “This is the last line of the script”,然后再发出啤酒信息。

1.4 reduce 操作符

reduce 操作符允许聚合可观察对象发出的值。它有两个参数:一个累加器函数和一个初始值。例如:

const beers = [
  {name: "Stella", country: "Belgium", price: 9.50},
  {name: "Sam Adams", country: "USA", price: 8.50},
  {name: "Bud Light", country: "USA", price: 6.50},
  {name: "Brooklyn Lager", country: "USA", price: 8.00},
  {name: "Sapporo", country: "Japan", price: 7.50}
];
from(beers)
 .pipe(
    map(beer => beer.price),
    reduce( (total, price) => total + price, 0)
  )
 .subscribe(
    totalPrice => console.log(`Total price: ${totalPrice}`)
  );

运行此脚本将输出所有啤酒的总价。 reduce 操作符在可观察对象完成时发出聚合结果。

2. 使用观察者 API

2.1 观察者的定义

观察者是实现了 next() error() complete() 函数中的一个或多个的对象。例如:

const beerObserver = {
  next: function(beer) { console.log(`Subscriber got ${beer.name}`)},
  error: function(err) { console.err(error)},
  complete: function() {console.log("The stream is over")}
}

2.2 可观察对象的创建

可以使用 create 方法创建可观察对象,传递一个代表观察者的参数:

const beerObservable$ = Observable.create( observer => observer.next(beer));

在订阅时,可观察对象才会知道具体提供的是哪个观察者。

2.3 完整示例

function getObservableBeer(){
  return Observable.create( observer => {
    const beers = [
      {name: "Stella", country: "Belgium", price: 9.50},
      {name: "Sam Adams", country: "USA", price: 8.50},
      {name: "Bud Light", country: "USA", price: 6.50},
      {name: "Brooklyn Lager", country: "USA", price: 8.00},
      {name: "Sapporo", country: "Japan", price: 7.50}
    ];
    beers.forEach( beer => observer.next(beer));
    observer.complete();
  });
}
getObservableBeer()
 .subscribe(
    beer => console.log(`Subscriber got ${beer.name}`),
    error => console.err(error),
    () => console.log("The stream is over")
  );

在这个示例中,我们创建了一个冷可观察对象,因为数据生产者(啤酒数组)是在 getObservableBeer() 可观察对象内部创建的。每个订阅者在查询条件相同时,无论订阅时间如何,都会得到相同的啤酒数据。

2.4 调试可观察对象

tap 操作符可以对源可观察对象发出的每个值执行副作用(例如记录一些数据),但返回的可观察对象与源对象相同,可用于调试:

import { map, tap } from 'rxjs/operators';
myObservable$
 .pipe(
    tap(beer => console.log(`Before: ${beer}`)),
    map(beer => `${beer.name}, ${beer.country}`),
    tap(beer => console.log(`After: ${beer}`))
  )
 .subscribe(...);

3. 使用 RxJS Subject

3.1 Subject 的概念

RxJS Subject 是一个包含可观察对象和观察者的对象。可以使用 next() 方法将数据推送给其观察者,也可以订阅它。Subject 可以有多个观察者,适用于多播场景,即向多个订阅者发出一个值。

3.2 简单示例

const mySubject$ = new Subject();
const subscription1 = mySubject$.subscribe(...);
const subscription2 = mySubject$.subscribe(...);
mySubject$.next(123); // each subscriber gets 123

3.3 实际应用示例

考虑一个金融公司的场景,交易员可以下达股票买卖订单,订单需要同时发送给两个脚本:一个用于向证券交易所下单,另一个用于向交易委员会报告订单。

enum Action{
  Buy = 'BUY',
  Sell = 'SELL'
}
class Order{
  constructor(public orderId: number, public traderId: number,
    public stock: string, public shares: number, public action:Action){}
}
const orders$ = new Subject<Order>();
class Trader {
  constructor(private traderId:number, private traderName:string){}
  placeOrder(order: Order){
    orders$.next(order);
  }
}
const stockExchange = orders$.subscribe(
  ord => console.log(`Sending to stock exchange the order to ${ord.action} ${ord.shares} shares of ${ord.stock}`)
);
const tradeCommission = orders$.subscribe(
  ord => console.log(`Reporting to trade commission the order to ${ord.action} ${ord.shares} shares of ${ord.stock}`)
);
const trader = new Trader(1, 'Joe');
const order1 = new Order(1, 1,'IBM',100,Action.Buy);
const order2 = new Order(2, 1,'AAPL',100,Action.Sell);
trader.placeOrder( order1);
trader.placeOrder( order2);

运行上述代码,两个订阅者都会收到交易员下达的订单信息。

3.4 总结

通过使用 RxJS Subject,可以方便地实现多播功能,将一个值同时发送给多个订阅者。在实际应用中,它可以用于处理需要向多个地方发送数据的场景,如金融交易中的订单处理。

3.5 流程图

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(Trader):::process -->|placeOrder| B(Subject - orders$):::process
    B -->|next| C(Stock Exchange Subscriber):::process
    B -->|next| D(Trade Commission Subscriber):::process

3.6 表格

元素 描述
Action 枚举 定义订单的操作类型,包括 Buy Sell
Order 表示订单,包含订单 ID、交易员 ID、股票名称、股数和操作类型
orders$ 主题 用于处理订单的主题,交易员下达的订单会通过它发送给订阅者
Trader 表示交易员,有 placeOrder 方法用于下达订单
stockExchange 订阅者 接收订单并向证券交易所发送订单信息
tradeCommission 订阅者 接收订单并向交易委员会报告订单信息

4. flatMap 操作符

4.1 flatMap 操作符的作用

在某些情况下,需要将可观察对象发出的每个项视为另一个可观察对象。 flatMap 操作符可以自动订阅外部可观察对象发出的每个项。它用于“将可观察对象发出的项转换为可观察对象,然后将这些可观察对象的发射结果展平为单个可观察对象”。

4.2 示例:处理饮料托盘

假设有一个可观察对象发出饮料托盘,每个托盘是一个可观察对象,我们要将这些托盘转换为包含单个饮料的输出流。

function getDrinks() {
  const beers$ = from([
    {name: "Stella", country: "Belgium", price: 9.50},
    {name: "Sam Adams", country: "USA", price: 8.50},
    {name: "Bud Light", country: "USA", price: 6.50}
  ], Scheduler.async);
  const softDrinks$ = from([
    {name: "Coca Cola", country: "USA", price: 1.50},
    {name: "Fanta", country: "USA", price: 1.50},
    {name: "Lemonade", country: "France", price: 2.50}
  ], Scheduler.async);
  return Observable.create( observer => {
    observer.next(beers$);
    observer.next(softDrinks$);
    observer.complete();
  });
}
// We want to "unload" each palette and print each drink info
getDrinks()
 .pipe(flatMap(drinks => drinks))
 .subscribe(
    drink => console.log(`Subscriber got ${drink.name}: ${drink.price}`),
    error => console.err(error),
    () => console.log("The stream of drinks is over")
  );

运行此脚本将输出每个饮料的信息。

4.3 示例:处理 HTTP 请求

在 Angular 中,HTTP 请求返回可观察对象。当需要执行多个 HTTP 请求,且第一个请求的结果要用于第二个请求时,可以使用 flatMap 操作符。

import {flatMap} from 'rxjs/operators';
httpClient.get('/customers/123')
 .pipe(
    flatMap(customer => this.httpClient.get(customer.orderUrl))
  )
 .subscribe(response => this.order = response);

这样可以避免嵌套的 subscribe() 调用。

4.4 示例:监控交易员订单

考虑一个场景,有两个 Subject 实例: traders$ 用于跟踪交易员, orders$ 用于跟踪每个交易员下达的订单。作为经理,想要监控所有交易员下达的订单。

enum Action{
  Buy = 'BUY',
  Sell = 'SELL'
}
class Order{
  constructor(public orderId: number, public traderId: number,
    public stock: string, public shares: number, public action: Action)
  {}
}
let traders$ = new Subject<Trader>();
class Trader {
  orders$ = new Subject<Order>();
  constructor(private traderId: number,
    public traderName: string) {}
}
let tradersSubscriber = traders$.subscribe
  (trader => console.log(`Trader ${trader.traderName} arrived`));
let ordersSubscriber = traders$
 .pipe(flatMap(trader => trader.orders$))
 .subscribe(ord =>
    console.log(`Got order from trader ${ord.traderId} to ${ord.action} ${ord.shares} shares of ${ord.stock}`)
  );
let firstTrader = new Trader(1, 'Joe');
let secondTrader = new Trader(2, 'Mary');
traders$.next(firstTrader);
traders$.next(secondTrader);
let order1 = new Order(1,1,'IBM',100,Action.Buy);
let order2 = new Order(2,1,'AAPL',200,Action.Sell);
let order3 = new Order(3,2,'MSFT',500,Action.Buy);
// Traders place orders
firstTrader.orders$.next(order1);
firstTrader.orders$.next(order2);
secondTrader.orders$.next(order3);

运行此代码,订阅者将收到所有交易员下达的订单信息。

4.5 流程图

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(traders$ 主题):::process -->|next| B(Trader 实例):::process
    B -->|orders$ 主题| C(flatMap 操作符):::process
    C -->|展平结果| D(ordersSubscriber 订阅者):::process

4.6 表格

元素 描述
Action 枚举 定义订单的操作类型,包括 Buy Sell
Order 表示订单,包含订单 ID、交易员 ID、股票名称、股数和操作类型
traders$ 主题 用于跟踪交易员的主题
Trader 表示交易员,每个交易员有自己的 orders$ 主题用于跟踪订单
orders$ 主题 每个交易员的主题,用于处理该交易员下达的订单
tradersSubscriber 订阅者 接收交易员到达的信息
ordersSubscriber 订阅者 接收所有交易员下达的订单信息

总结

本文详细介绍了 RxJS 的一些核心概念和操作符,包括操作符的组合、可管道操作符、 reduce 操作符、观察者 API、RxJS Subject 和 flatMap 操作符。通过具体的代码示例和流程图、表格,展示了这些概念和操作符的使用方法和应用场景。掌握这些知识可以帮助开发者更好地处理异步数据流,实现复杂的响应式编程。

操作步骤总结

  • 操作符组合 :使用 pipe() 方法将多个操作符组合起来处理可观察对象发出的项。
  • 可管道操作符 :使用 ES6 导入语法导入操作符,然后将它们包装在 pipe() 函数中,便于打包工具进行树摇优化。
  • reduce 操作符 :通过指定累加器函数和初始值,聚合可观察对象发出的值。
  • 观察者 API :创建观察者对象,实现 next() error() complete() 函数,使用 create 方法创建可观察对象并订阅。
  • RxJS Subject :创建 Subject 实例,使用 next() 方法推送数据,多个订阅者可以接收相同的值。
  • flatMap 操作符 :自动订阅外部可观察对象发出的每个项,将内部可观察对象的发射结果展平为单个可观察对象。

通过实际代码示例和操作步骤的总结,希望读者能够更好地理解和应用 RxJS 的相关知识。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值