看到前端框架用到的一个js的库 -- RxJS


今天有同事问了个问题,下边图片的of是干什么用的

 然后呢,我就发现了 RxJS

看了一遍文档之后,感觉和Java的stream很像的东西。

RxJS结合Angular的使用方式为

https://blog.csdn.net/xiaozhen0610/article/details/108123093

下边为博客中的内容,防止链接丢失,贴上

一 map操作符

类似于大家所熟知的 

Array.prototype.map

 方法,此操作符将投射函数应用于每个值 并且在输出 Observable 中发出投射后的结果。

import { Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { of } from 'rxjs/observable/of';
    import { map } from 'rxjs/operators/map';
     
    @Component({
    selector: 'app-convert',
    templateUrl: './convert.component.html',
    styleUrls: ['./convert.component.css']
    })
    export class ConvertComponent implements OnInit {
     
    constructor() { }
     
    ngOnInit() {
    of(1, 2).pipe(map(val => val * 10))
    .subscribe(val => {
    console.log(val);
    });
    }
     
    }

Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。

可以把 RxJS 当做是用来处理事件的 Lodash 。

ReactiveX 结合了 观察者模式、迭代器模式 和 使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。

在 RxJS 中用来解决异步事件管理的的基本概念是:

  • Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
  • Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 mapfilterconcatflatMap 等这样的操作符来处理集合。
  • Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
  • Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeoutrequestAnimationFrame 或其他。

第一个示例

注册事件监听器的常规写法。

var button = document.querySelector('button');
button.addEventListener('click', () => console.log('Clicked!'));

使用 RxJS 的话,创建一个 observable 来代替。

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .subscribe(() => console.log('Clicked!'));

纯净性 (Purity)

使得 RxJS 强大的正是它使用纯函数来产生值的能力。这意味着你的代码更不容易出错。

通常你会创建一个非纯函数,在这个函数之外也使用了共享变量的代码,这将使得你的应用状态一团糟。

var count = 0;
var button = document.querySelector('button');
button.addEventListener('click', () => console.log(`Clicked ${++count} times`));

使用 RxJS 的话,你会将应用状态隔离出来。

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .scan(count => count + 1, 0)
  .subscribe(count => console.log(`Clicked ${count} times`));

scan 操作符的工作原理与数组的 reduce 类似。它需要一个暴露给回调函数当参数的初始值。每次回调函数运行后的返回值会作为下次回调函数运行时的参数。

流动性 (Flow)

RxJS 提供了一整套操作符来帮助你控制事件如何流经 observables 。

下面的代码展示的是如何控制一秒钟内最多点击一次,先来看使用普通的 JavaScript:

var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', () => {
  if (Date.now() - lastClick >= rate) {
    console.log(`Clicked ${++count} times`);
    lastClick = Date.now();
  }
});

使用 RxJS:

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .throttleTime(1000)
  .scan(count => count + 1, 0)
  .subscribe(count => console.log(`Clicked ${count} times`));

其他流程控制操作符有 filterdelaydebounceTimetaketakeUntildistinctdistinctUntilChanged 等等。

值 (Values)

对于流经 observables 的值,你可以对其进行转换。

下面的代码展示的是如何累加每次点击的鼠标 x 坐标,先来看使用普通的 JavaScript:

var count = 0;
var rate = 1000;
var lastClick = Date.now() - rate;
var button = document.querySelector('button');
button.addEventListener('click', (event) => {
  if (Date.now() - lastClick >= rate) {
    count += event.clientX;
    console.log(count)
    lastClick = Date.now();
  }
});

使用 RxJS:

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  .throttleTime(1000)
  .map(event => event.clientX)
  .scan((count, clientX) => count + clientX, 0)
  .subscribe(count => console.log(count));

其他产生值的操作符有 pluckpairwisesample 等等。

Observable (可观察对象)

Observables 是多个值的惰性推送集合。它填补了下面表格中的空白:

 单个值多个值
拉取 Function Iterator
推送 Promise Observable

示例 - 当订阅下面代码中的 Observable 的时候会立即(同步地)推送值123,然后1秒后会推送值4,再然后是完成流:

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});

要调用 Observable 并看到这些值,我们需要订阅 Observable:

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});

console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

控制台执行的结果:

just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done

拉取 (Pull) vs. 推送 (Push)

拉取推送是两种不同的协议,用来描述数据生产者 (Producer)如何与数据消费者 (Consumer)进行通信的。

什么是拉取? - 在拉取体系中,由消费者来决定何时从生产者那里接收数据。生产者本身不知道数据是何时交付到消费者手中的。

每个 JavaScript 函数都是拉取体系。函数是数据的生产者,调用该函数的代码通过从函数调用中“取出”一个单个返回值来对该函数进行消费。

ES2015 引入了 generator 函数和 iterators (function*),这是另外一种类型的拉取体系。调用 iterator.next() 的代码是消费者,它会从 iterator(生产者) 那“取出”多个值。

 生产者消费者
拉取 被动的: 当被请求时产生数据。 主动的: 决定何时请求数据。
推送 主动的: 按自己的节奏产生数据。 被动的: 对收到的数据做出反应。

什么是推送? - 在推送体系中,由生产者来决定何时把数据发送给消费者。消费者本身不知道何时会接收到数据。

在当今的 JavaScript 世界中,Promises 是最常见的推送体系类型。Promise(生产者) 将一个解析过的值传递给已注册的回调函数(消费者),但不同于函数的是,由 Promise 来决定何时把值“推送”给回调函数。

RxJS 引入了 Observables,一个新的 JavaScript 推送体系。Observable 是多个值的生产者,并将值“推送”给观察者(消费者)。

  • Function 是惰性的评估运算,调用时会同步地返回一个单一值。
  • Generator 是惰性的评估运算,调用时会同步地返回零到(有可能的)无限多个值。
  • Promise 是最终可能(或可能不)返回单个值的运算。
  • Observable 是惰性的评估运算,它可以从它被调用的时刻起同步或异步地返回零到(有可能的)无限多个值。

Observables 作为函数的泛化

与流行的说法正好相反,Observables 既不像 EventEmitters,也不像多个值的 Promises 。在某些情况下,即当使用 RxJS 的 Subjects 进行多播时, Observables 的行为可能会比较像 EventEmitters,但通常情况下 Observables 的行为并不像 EventEmitters 。

Observables 像是没有参数, 但可以泛化为多个值的函数。

考虑如下代码:

function foo() {
  console.log('Hello');
  return 42;
}

var x = foo.call(); // 等同于 foo()
console.log(x);
var y = foo.call(); // 等同于 foo()
console.log(y);

我们期待看到的输出:

"Hello"
42
"Hello"
42

你可以使用 Observables 重写上面的代码:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
});

foo.subscribe(function (x) {
  console.log(x);
});
foo.subscribe(function (y) {
  console.log(y);
});

输出是一样的:

"Hello"
42
"Hello"
42

这是因为函数和 Observables 都是惰性运算。如果你不调用函数,console.log('Hello') 就不会执行。Observables 也是如此,如果你不“调用”它(使用 subscribe),console.log('Hello') 也不会执行。此外,“调用”或“订阅”是独立的操作:两个函数调用会触发两个单独的副作用,两个 Observable 订阅同样也是触发两个单独的副作用。EventEmitters 共享副作用并且无论是否存在订阅者都会尽早执行,Observables 与之相反,不会共享副作用并且是延迟执行。

订阅 Observable 类似于调用函数。

一些人声称 Observables 是异步的。那不是真的。如果你用日志包围一个函数调用,像这样:

console.log('before');
console.log(foo.call());
console.log('after');

你会看到这样的输出:

"before"
"Hello"
42
"after"

使用 Observables 来做同样的事:

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

输出是:

"before"
"Hello"
42
"after"

这证明了 foo 的订阅完全是同步的,就像函数一样。

Observables 传递值可以是同步的,也可以是异步的。

那么 Observable 和 函数的区别是什么呢?Observable 可以随着时间的推移“返回”多个值,这是函数所做不到的。你无法这样:

function foo() {
  console.log('Hello');
  return 42;
  return 100; // 死代码,永远不会执行
}

函数只能返回一个值。但 Observables 可以这样:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
  observer.next(100); // “返回”另外一个值
  observer.next(200); // 还可以再“返回”值
});

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

同步输出:

"before"
"Hello"
42
100
200
"after"

但你也可以异步地“返回”值:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
  observer.next(100);
  observer.next(200);
  setTimeout(() => {
    observer.next(300); // 异步执行
  }, 1000);
});

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

输出:

"before"
"Hello"
42
100
200
"after"
300

结论:

  • func.call() 意思是 "同步地给我一个值"
  • observable.subscribe() 意思是 "给我任意数量的值,无论是同步还是异步"

Observable 剖析

Observables 是使用 Rx.Observable.create 或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理。这四个方面全部编码在 Observables 实例中,但某些方面是与其他类型相关的,像 Observer (观察者) 和 Subscription (订阅)。

Observable 的核心关注点:

  • 创建 Observables
  • 订阅 Observables
  • 执行 Observables
  • 清理 Observables

创建 Observables

Rx.Observable.createObservable 构造函数的别名,它接收一个参数:subscribe 函数。

下面的示例创建了一个 Observable,它每隔一秒会向观察者发送字符串 'hi'

var observable = Rx.Observable.create(function subscribe(observer) {
  var id = setInterval(() => {
    observer.next('hi')
  }, 1000);
});

Observables 可以使用 create 来创建, 但通常我们使用所谓的创建操作符, 像 offrominterval、等等。

在上面的示例中,subscribe 函数是用来描述 Observable 最重要的一块。我们来看下订阅是什么意思。

订阅 Observables

示例中的 Observable 对象 observable 可以订阅,像这样:

observable.subscribe(x => console.log(x));

observable.subscribeObservable.create(function subscribe(observer) {...}) 中的 subscribe 有着同样的名字,这并不是一个巧合。在库中,它们是不同的,但从实际出发,你可以认为在概念上它们是等同的。

这表明 subscribe 调用在同一 Observable 的多个观察者之间是不共享的。当使用一个观察者调用 observable.subscribe 时,Observable.create(function subscribe(observer) {...}) 中的 subscribe 函数只服务于给定的观察者。对 observable.subscribe 的每次调用都会触发针对给定观察者的独立设置。

订阅 Observable 像是调用函数, 并提供接收数据的回调函数。

这与像 addEventListener / removeEventListener 这样的事件处理方法 API 是完全不同的。使用 observable.subscribe,在 Observable 中不会将给定的观察者注册为监听器。Observable 甚至不会去维护一个附加的观察者列表。

subscribe 调用是启动 “Observable 执行”的一种简单方式, 并将值或事件传递给本次执行的观察者。

执行 Observables

Observable.create(function subscribe(observer) {...})...的代码表示 “Observable 执行”,它是惰性运算,只有在每个观察者订阅后才会执行。随着时间的推移,执行会以同步或异步的方式产生多个值。

Observable 执行可以传递三种类型的值:

  • "Next" 通知: 发送一个值,比如数字、字符串、对象,等等。
  • "Error" 通知: 发送一个 JavaScript 错误 或 异常。
  • "Complete" 通知: 不再发送任何值。

"Next" 通知是最重要,也是最常见的类型:它们表示传递给观察者的实际数据。"Error" 和 "Complete" 通知可能只会在 Observable 执行期间发生一次,并且只会执行其中的一个。

这些约束用所谓的 Observable 语法合约表达最好,写为正则表达式是这样的:

next*(error|complete)?

在 Observable 执行中, 可能会发送零个到无穷多个 "Next" 通知。如果发送的是 "Error" 或 "Complete" 通知的话,那么之后不会再发送任何通知了。

下面是 Observable 执行的示例,它发送了三个 "Next" 通知,然后是 "Complete" 通知:

var observable = Rx.Observable.create(function subscribe(observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

Observable 严格遵守自身的规约,所以下面的代码不会发送 "Next" 通知 4

var observable = Rx.Observable.create(function subscribe(observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
  observer.next(4); // 因为违反规约,所以不会发送
});

subscribe 中用 try/catch 代码块来包裹任意代码是个不错的主意,如果捕获到异常的话,会发送 "Error" 通知:

var observable = Rx.Observable.create(function subscribe(observer) {
  try {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
  } catch (err) {
    observer.error(err); // 如果捕获到异常会发送一个错误
  }
});

清理 Observable 执行

因为 Observable 执行可能会是无限的,并且观察者通常希望能在有限的时间内中止执行,所以我们需要一个 API 来取消执行。因为每个执行都是其对应观察者专属的,一旦观察者完成接收值,它必须要一种方法来停止执行,以避免浪费计算能力或内存资源。

当调用了 observable.subscribe ,观察者会被附加到新创建的 Observable 执行中。这个调用还返回一个对象,即 Subscription (订阅):

var subscription = observable.subscribe(x => console.log(x));

Subscription 表示进行中的执行,它有最小化的 API 以允许你取消执行。想了解更多订阅相关的内容,请参见 Subscription 类型。使用 subscription.unsubscribe() 你可以取消进行中的执行:

var observable = Rx.Observable.from([10, 20, 30]);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
subscription.unsubscribe();

当你订阅了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe() 方法就可以取消执行。

当我们使用 create() 方法创建 Observable 时,Observable 必须定义如何清理执行的资源。你可以通过在 function subscribe() 中返回一个自定义的 unsubscribe 函数。

举例来说,这是我们如何清理使用了 setInterval 的 interval 执行集合:

var observable = Rx.Observable.create(function subscribe(observer) {
  // 追踪 interval 资源
  var intervalID = setInterval(() => {
    observer.next('hi');
  }, 1000);

  // 提供取消和清理 interval 资源的方法
  return function unsubscribe() {
    clearInterval(intervalID);
  };
});

正如 observable.subscribe 类似于 Observable.create(function subscribe() {...}),从 subscribe 返回的 unsubscribe 在概念上也等同于 subscription.unsubscribe。事实上,如果我们抛开围绕这些概念的 ReactiveX 类型,保留下来的只是相当简单的 JavaScript 。

function subscribe(observer) {
  var intervalID = setInterval(() => {
    observer.next('hi');
  }, 1000);

  return function unsubscribe() {
    clearInterval(intervalID);
  };
}

var unsubscribe = subscribe({next: (x) => console.log(x)});

// 稍后:
unsubscribe(); // 清理资源

为什么我们要使用像 Observable、Observer 和 Subscription 这样的 Rx 类型?原因是保证代码的安全性(比如 Observable 规约)和操作符的可组合性。

Observer (观察者)

什么是观察者? - 观察者是由 Observable 发送的值的消费者。观察者只是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:nexterrorcomplete 。下面的示例是一个典型的观察者对象:

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

要使用观察者,需要把它提供给 Observable 的 subscribe 方法:

observable.subscribe(observer);

观察者只是有三个回调函数的对象,每个回调函数对应一种 Observable 发送的通知类型。

RxJS 中的观察者也可能是部分的。如果你没有提供某个回调函数,Observable 的执行也会正常运行,只是某些通知类型会被忽略,因为观察者中没有相对应的回调函数。

下面的示例是没有 complete 回调函数的观察者:

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
};

当订阅 Observable 时,你可能只提供了一个回调函数作为参数,而并没有将其附加到观察者对象上,例如这样:

observable.subscribe(x => console.log('Observer got a next value: ' + x));

observable.subscribe 内部,它会创建一个观察者对象并使用第一个回调函数参数作为 next 的处理方法。三种类型的回调函数都可以直接作为参数来提供:

observable.subscribe(
  x => console.log('Observer got a next value: ' + x),
  err => console.error('Observer got an error: ' + err),
  () => console.log('Observer got a complete notification')
);

Subscription (订阅)

什么是 Subscription ? - Subscription 是表示可清理资源的对象,通常是 Observable 的执行。Subscription 有一个重要的方法,即 unsubscribe,它不需要任何参数,只是用来清理由 Subscription 占用的资源。在上一个版本的 RxJS 中,Subscription 叫做 "Disposable" (可清理对象)。

var observable = Rx.Observable.interval(1000);
var subscription = observable.subscribe(x => console.log(x));
// 稍后:
// 这会取消正在进行中的 Observable 执行
// Observable 执行是通过使用观察者调用 subscribe 方法启动的
subscription.unsubscribe();

Subscription 基本上只有一个 unsubscribe() 函数,这个函数用来释放资源或去取消 Observable 执行。

Subscription 还可以合在一起,这样一个 Subscription 调用 unsubscribe() 方法,可能会有多个 Subscription 取消订阅 。你可以通过把一个 Subscription 添加到另一个上面来做这件事:

var observable1 = Rx.Observable.interval(400);
var observable2 = Rx.Observable.interval(300);

var subscription = observable1.subscribe(x => console.log('first: ' + x));
var childSubscription = observable2.subscribe(x => console.log('second: ' + x));

subscription.add(childSubscription);

setTimeout(() => {
  // subscription 和 childSubscription 都会取消订阅
  subscription.unsubscribe();
}, 1000);

执行时,我们在控制台中看到:

second: 0
first: 0
second: 1
first: 1
second: 2

Subscriptions 还有一个 remove(otherSubscription) 方法,用来撤销一个已添加的子 Subscription 。

Subject (主体)

什么是 Subject? - RxJS Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。

Subject 像是 Observable,但是可以多播给多个观察者。Subject 还像是 EventEmitters,维护着多个监听器的注册表。

每个 Subject 都是 Observable 。 - 对于 Subject,你可以提供一个观察者并使用 subscribe 方法,就可以开始正常接收值。从观察者的角度而言,它无法判断 Observable 执行是来自普通的 Observable 还是 Subject 。

在 Subject 的内部,subscribe 不会调用发送值的新执行。它只是将给定的观察者注册到观察者列表中,类似于其他库或语言中的 addListener 的工作方式。

每个 Subject 都是观察者。 - Subject 是一个有如下方法的对象: next(v)error(e)complete() 。要给 Subject 提供新值,只要调用 next(theValue),它会将值多播给已注册监听该 Subject 的观察者们。

在下面的示例中,我们为 Subject 添加了两个观察者,然后给 Subject 提供一些值:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(1);
subject.next(2);

下面是控制台的输出:

observerA: 1
observerB: 1
observerA: 2
observerB: 2

因为 Subject 是观察者,这也就在意味着你可以把 Subject 作为参数传给任何 Observable 的 subscribe 方法,如下面的示例所展示的:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

var observable = Rx.Observable.from([1, 2, 3]);

observable.subscribe(subject); // 你可以提供一个 Subject 进行订阅

执行结果:

observerA: 1
observerB: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

使用上面的方法,我们基本上只是通过 Subject 将单播的 Observable 执行转换为多播的。这也说明了 Subjects 是将任意 Observable 执行共享给多个观察者的唯一方式。

还有一些特殊类型的 SubjectBehaviorSubjectReplaySubjectAsyncSubject

多播的 Observables

“多播 Observable” 通过 Subject 来发送通知,这个 Subject 可能有多个订阅者,然而普通的 “单播 Observable” 只发送通知给单个观察者。

多播 Observable 在底层是通过使用 Subject 使得多个观察者可以看见同一个 Observable 执行。

在底层,这就是 multicast 操作符的工作原理:观察者订阅一个基础的 Subject,然后 Subject 订阅源 Observable 。下面的示例与前面使用 observable.subscribe(subject) 的示例类似:

var source = Rx.Observable.from([1, 2, 3]);
var subject = new Rx.Subject();
var multicasted = source.multicast(subject);

// 在底层使用了 `subject.subscribe({...})`:
multicasted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
multicasted.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

// 在底层使用了 `source.subscribe(subject)`:
multicasted.connect();

multicast 操作符返回一个 Observable,它看起来和普通的 Observable 没什么区别,但当订阅时就像是 Subject 。multicast 返回的是 ConnectableObservable,它只是一个有 connect() 方法的 Observable 。

connect() 方法十分重要,它决定了何时启动共享的 Observable 执行。因为 connect() 方法在底层执行了 source.subscribe(subject),所以它返回的是 Subscription,你可以取消订阅以取消共享的 Observable 执行。

引用计数

手动调用 connect() 并处理 Subscription 通常太笨重。通常,当第一个观察者到达时我们想要自动地连接,而当最后一个观察者取消订阅时我们想要自动地取消共享执行。

请考虑以下示例,下面的列表概述了 Subscriptions 发生的经过:

  1. 第一个观察者订阅了多播 Observable
  2. 多播 Observable 已连接
  3. next0 发送给第一个观察者
  4. 第二个观察者订阅了多播 Observable
  5. next1 发送给第一个观察者
  6. next1 发送给第二个观察者
  7. 第一个观察者取消了多播 Observable 的订阅
  8. next2 发送给第二个观察者
  9. 第二个观察者取消了多播 Observable 的订阅
  10. 多播 Observable 的连接已中断(底层进行的操作是取消订阅)

要实现这点,需要显式地调用 connect(),代码如下:

var source = Rx.Observable.interval(500);
var subject = new Rx.Subject();
var multicasted = source.multicast(subject);
var subscription1, subscription2, subscriptionConnect;

subscription1 = multicasted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
// 这里我们应该调用 `connect()`,因为 `multicasted` 的第一个
// 订阅者关心消费值
subscriptionConnect = multicasted.connect();

setTimeout(() => {
  subscription2 = multicasted.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 600);

setTimeout(() => {
  subscription1.unsubscribe();
}, 1200);

// 这里我们应该取消共享的 Observable 执行的订阅,
// 因为此后 `multicasted` 将不再有订阅者
setTimeout(() => {
  subscription2.unsubscribe();
  subscriptionConnect.unsubscribe(); // 用于共享的 Observable 执行
}, 2000);

如果不想显式调用 connect(),我们可以使用 ConnectableObservable 的 refCount() 方法(引用计数),这个方法返回 Observable,这个 Observable 会追踪有多少个订阅者。当订阅者的数量从0变成1,它会调用 connect() 以开启共享的执行。当订阅者数量从1变成0时,它会完全取消订阅,停止进一步的执行。

refCount 的作用是,当有第一个订阅者时,多播 Observable 会自动地启动执行,而当最后一个订阅者离开时,多播 Observable 会自动地停止执行。

示例如下:

var source = Rx.Observable.interval(500);
var subject = new Rx.Subject();
var refCounted = source.multicast(subject).refCount();
var subscription1, subscription2, subscriptionConnect;

// 这里其实调用了 `connect()`,
// 因为 `refCounted` 有了第一个订阅者
console.log('observerA subscribed');
subscription1 = refCounted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

setTimeout(() => {
  console.log('observerB subscribed');
  subscription2 = refCounted.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 600);

setTimeout(() => {
  console.log('observerA unsubscribed');
  subscription1.unsubscribe();
}, 1200);

// 这里共享的 Observable 执行会停止,
// 因为此后 `refCounted` 将不再有订阅者
setTimeout(() => {
  console.log('observerB unsubscribed');
  subscription2.unsubscribe();
}, 2000);

执行结果:

observerA subscribed
observerA: 0
observerB subscribed
observerA: 1
observerB: 1
observerA unsubscribed
observerB: 2
observerB unsubscribed

refCount() 只存在于 ConnectableObservable,它返回的是 Observable,而不是另一个 ConnectableObservable 。

BehaviorSubject

Subject 的其中一个变体就是 BehaviorSubject,它有一个“当前值”的概念。它保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。

BehaviorSubjects 适合用来表示“随时间推移的值”。举例来说,生日的流是一个 Subject,但年龄的流应该是一个 BehaviorSubject 。

在下面的示例中,BehaviorSubject 使用值0进行初始化,当第一个观察者订阅时会得到0。第二个观察者订阅时会得到值2,尽管它是在值2发送之后订阅的。

var subject = new Rx.BehaviorSubject(0); // 0是初始值

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(3);

输出:

observerA: 0
observerA: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

ReplaySubject

ReplaySubject 类似于 BehaviorSubject,它可以发送旧值给新的订阅者,但它还可以记录 Observable 执行的一部分。

ReplaySubject 记录 Observable 执行中的多个值并将其回放给新的订阅者。

当创建 ReplaySubject 时,你可以指定回放多少个值:

var subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);

With output:

observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerB: 2
observerB: 3
observerB: 4
observerA: 5
observerB: 5

除了缓冲数量,你还可以指定 window time (以毫秒为单位)来确定多久之前的值可以记录。在下面的示例中,我们使用了较大的缓存数量100,但 window time 参数只设置了500毫秒。

var subject = new Rx.ReplaySubject(100, 500 /* windowTime */);

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

var i = 1;
setInterval(() => subject.next(i++), 200);

setTimeout(() => {
  subject.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 1000);

从下面的输出可以看出,第二个观察者得到的值是345,这三个值是订阅发生前的500毫秒内发生的:

observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerA: 5
observerB: 3
observerB: 4
observerB: 5
observerA: 6
observerB: 6
...

AsyncSubject

AsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。

var subject = new Rx.AsyncSubject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);
subject.complete();

输出:

observerA: 5
observerB: 5

AsyncSubject 和 last() 操作符类似,因为它也是等待 complete 通知,以发送一个单个值。

Operators (操作符)

尽管 RxJS 的根基是 Observable,但最有用的还是它的操作符。操作符是允许复杂的异步代码以声明式的方式进行轻松组合的基础代码单元。

什么是操作符?

操作符是 Observable 类型上的方法,比如 .map(...).filter(...).merge(...),等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable 。

操作符是函数,它基于当前的 Observable 创建一个新的 Observable。这是一个无副作用的操作:前面的 Observable 保持不变。

操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。订阅输出 Observable 同样会订阅输入 Observable 。在下面的示例中,我们创建一个自定义操作符函数,它将从输入 Observable 接收的每个值都乘以10:

function multiplyByTen(input) {
  var output = Rx.Observable.create(function subscribe(observer) {
    input.subscribe({
      next: (v) => observer.next(10 * v),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
  });
  return output;
}

var input = Rx.Observable.from([1, 2, 3, 4]);
var output = multiplyByTen(input);
output.subscribe(x => console.log(x));

输出:

10
20
30
40

注意,订阅 output 会导致 input Observable 也被订阅。我们称之为“操作符订阅链”。

实例操作符 vs. 静态操作符

什么是实例操作符? - 通常提到操作符时,我们指的是实例操作符,它是 Observable 实例上的方法。举例来说,如果上面的 multiplyByTen 是官方提供的实例操作符,它看起来大致是这个样子的:

Rx.Observable.prototype.multiplyByTen = function multiplyByTen() {
  var input = this;
  return Rx.Observable.create(function subscribe(observer) {
    input.subscribe({
      next: (v) => observer.next(10 * v),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
  });
}

实例运算符是使用 this 关键字来指代输入的 Observable 的函数。

注意,这里的 input Observable 不再是一个函数参数,它现在是 this 对象。下面是我们如何使用这样的实例运算符:

var observable = Rx.Observable.from([1, 2, 3, 4]).multiplyByTen();

observable.subscribe(x => console.log(x));

什么是静态操作符? - 除了实例操作符,还有静态操作符,它们是直接附加到 Observable 类上的。静态操作符在内部不使用 this 关键字,而是完全依赖于它的参数。

静态操作符是附加到 Observalbe 类上的纯函数,通常用来从头开始创建 Observalbe 。

最常用的静态操作符类型是所谓的创建操作符。它们只接收非 Observable 参数,比如数字,然后创建一个新的 Observable ,而不是将一个输入 Observable 转换为输出 Observable 。

一个典型的静态操作符例子就是 interval 函数。它接收一个数字(非 Observable)作为参数,并生产一个 Observable 作为输出:

var observable = Rx.Observable.interval(1000 /* 毫秒数 */);

创建操作符的另一个例子就是 create,已经在前面的示例中广泛使用。点击这里查看所有静态操作符列表。

然而,有些静态操作符可能不同于简单的创建。一些组合操作符可能是静态的,比如 mergecombineLatestconcat,等等。这些作为静态运算符是有道理的,因为它们将多个 Observables 作为输入,而不仅仅是一个,例如:

var observable1 = Rx.Observable.interval(1000);
var observable2 = Rx.Observable.interval(400);

var merged = Rx.Observable.merge(observable1, observable2);

Marble diagrams (弹珠图)

要解释操作符是如何工作的,文字描述通常是不足以描述清楚的。许多操作符都是跟时间相关的,它们可能会以不同的方式延迟(delay)、取样(sample)、节流(throttle)或去抖动值(debonce)。图表通常是更适合的工具。弹珠图是操作符运行方式的视觉表示,其中包含输入 Obserable(s) (输入可能是多个 Observable )、操作符及其参数和输出 Observable 。

在弹珠图中,时间流向右边,图描述了在 Observable 执行中值(“弹珠”)是如何发出的。

在下图中可以看到解剖过的弹珠图。

在整个文档站中,我们广泛地使用弹珠图来解释操作符的工作方式。它们在其他环境中也可能非常有用,例如在白板上,甚至在我们的单元测试中(如 ASCII 图)。

选择操作符

你需要找到一个操作符来解决你的问题吗? 从下面的列表中选择一个选项开始:

  • 我有一个已存在的 Observable, 然后...
  • 我把一些 Observables 组合成了一个 Observable,然后...
  • 我还没有 Observable, 然后...

操作符分类

操作符有着不同的用途,它们可作如下分类:创建、转换、过滤、组合、错误处理、工具,等等。在下面的列表中,你可以按分类组织好的所有操作符。

创建操作符

  • ajax
  • bindCallback
  • bindNodeCallback
  • create
  • defer
  • empty
  • from
  • fromEvent
  • fromEventPattern
  • fromPromise
  • generate
  • interval
  • never
  • of
  • repeat
  • repeatWhen
  • range
  • throw
  • timer

转换操作符

  • buffer
  • bufferCount
  • bufferTime
  • bufferToggle
  • bufferWhen
  • concatMap
  • concatMapTo
  • exhaustMap
  • expand
  • groupBy
  • map
  • mapTo
  • mergeMap
  • mergeMapTo
  • mergeScan
  • pairwise
  • partition
  • pluck
  • scan
  • switchMap
  • switchMapTo
  • window
  • windowCount
  • windowTime
  • windowToggle
  • windowWhen

过滤操作符

  • debounce
  • debounceTime
  • distinct
  • distinctKey
  • distinctUntilChanged
  • distinctUntilKeyChanged
  • elementAt
  • filter
  • first
  • ignoreElements
  • audit
  • auditTime
  • last
  • sample
  • sampleTime
  • single
  • skip
  • skipLast
  • skipUntil
  • skipWhile
  • take
  • takeLast
  • takeUntil
  • takeWhile
  • throttle
  • throttleTime

组合操作符

  • combineAll
  • combineLatest
  • concat
  • concatAll
  • exhaust
  • forkJoin
  • merge
  • mergeAll
  • race
  • startWith
  • switch
  • withLatestFrom
  • zip
  • zipAll

多播操作符

  • cache
  • multicast
  • publish
  • publishBehavior
  • publishLast
  • publishReplay
  • share

错误处理操作符

  • catch
  • retry
  • retryWhen

工具操作符

  • do
  • delay
  • delayWhen
  • dematerialize
  • finally
  • let
  • materialize
  • observeOn
  • subscribeOn
  • timeInterval
  • timestamp
  • timeout
  • timeoutWith
  • toArray
  • toPromise

条件和布尔操作符

  • defaultIfEmpty
  • every
  • find
  • findIndex
  • isEmpty

数学和聚合操作符

  • count
  • max
  • min
  • reduce

Scheduler (调度器)

什么是调度器? - 调度器控制着何时启动 subscription 和何时发送通知。它由三部分组成:

  • 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
  • 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。
  • 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法 now() 提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。

调度器可以让你规定 Observable 在什么样的执行上下文中发送通知给它的观察者。

在下面的示例中,我们采用普通的 Observable ,它同步地发出值123,并使用操作符 observeOn 来指定 async 调度器发送这些值。

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
})
.observeOn(Rx.Scheduler.async);

console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

输出结果:

just before subscribe
just after subscribe
got value 1
got value 2
got value 3
done

注意通知 got value...just after subscribe 之后才发送,这与我们到目前为止所见的默认行为是不一样的。这是因为 observeOn(Rx.Scheduler.async)Observable.create 和最终的观察者之间引入了一个代理观察者。在下面的示例代码中,我们重命名了一些标识符,使得其中的区别变得更明显:

var observable = Rx.Observable.create(function (proxyObserver) {
  proxyObserver.next(1);
  proxyObserver.next(2);
  proxyObserver.next(3);
  proxyObserver.complete();
})
.observeOn(Rx.Scheduler.async);

var finalObserver = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};

console.log('just before subscribe');
observable.subscribe(finalObserver);
console.log('just after subscribe');

proxyObserver 是在 observeOn(Rx.Scheduler.async) 中创建的,它的 next(val) 函数大概是下面这样子的:

var proxyObserver = {
  next: (val) => {
    Rx.Scheduler.async.schedule(
      (x) => finalObserver.next(x),
      0 /* 延迟时间 */,
      val /* 会作为上面函数所使用的 x */
    );
  },

  // ...
}

async 调度器操作符使用了 setTimeoutsetInterval,即使给定的延迟时间为0。照例,在 JavaScript 中,我们已知的是 setTimeout(fn, 0) 会在下一次事件循环迭代的最开始运行 fn 。这也解释了为什么发送给 finalObservergot value 1 发生在 just after subscribe 之后。

调度器的 schedule() 方法接收一个 delay 参数,它指的是相对于调度器内部时钟的一段时间。调度器的时钟不需要与实际的挂钟时间有任何关系。这也就是为什么像 delay 这样的时间操作符不是在实际时间上操作的,而是取决于调度器的时钟时间。这在测试中极其有用,可以使用虚拟时间调度器来伪造挂钟时间,同时实际上是在同步执行计划任务。

调度器类型

async 调度器是 RxJS 提供的内置调度器中的一个。可以通过使用 Scheduler 对象的静态属性创建并返回其中的每种类型的调度器。

调度器目的
null 不传递任何调度器的话,会以同步递归的方式发送通知。用于定时操作或尾递归操作。
Rx.Scheduler.queue 当前事件帧中的队列调度(蹦床调度器)。用于迭代操作。
Rx.Scheduler.asap 微任务的队列调度,它使用可用的最快速的传输机制,比如 Node.js 的 process.nextTick() 或 Web Worker 的 MessageChannel 或 setTimeout 或其他。用于异步转换。
Rx.Scheduler.async 使用 setInterval 的调度。用于基于时间的操作符。

使用调度器

你可能在你的 RxJS 代码中已经使用过调度器了,只是没有明确地指明要使用的调度器的类型。这是因为所有的 Observable 操作符处理并发性都有可选的调度器。如果没有提供调度器的话,RxJS 会通过使用最小并发原则选择一个默认调度器。这意味着引入满足操作符需要的最小并发量的调度器会被选择。例如,对于返回有限和少量消息的 observable 的操作符,RxJS 不使用调度器,即 nullundefined 。对于返回潜在大量的或无限数量的消息的操作符,使用 queue 调度器。对于使用定时器的操作符,使用 aysnc 调度器。

因为 RxJS 使用最少的并发调度器,如果出于性能考虑,你想要引入并发,那么可以选择不同的调度器。要指定具体的调度器,可以使用那些采用调度器的操作符方法,例如 from([10, 20, 30], Rx.Scheduler.async)

静态创建操作符通常可以接收调度器作为参数。 举例来说,from(array, scheduler) 可以让你指定调度器,当发送从 array 转换的每个通知的时候使用。调度器通常作为操作符的最后一个参数。下面的静态创建操作符接收调度器参数:

  • bindCallback
  • bindNodeCallback
  • combineLatest
  • concat
  • empty
  • from
  • fromPromise
  • interval
  • merge
  • of
  • range
  • throw
  • timer

使用 subscribeOn 来调度 subscribe() 调用在什么样的上下文中执行。 默认情况下,Observable 的 subscribe() 调用会立即同步地执行。然而,你可能会延迟或安排在给定的调度器上执行实际的 subscription ,使用实例操作符 subscribeOn(scheduler),其中 scheduler 是你提供的参数。

使用 observeOn 来调度发送通知的的上下文。 正如我们在上面的示例中所看到的,实例操作符 observeOn(scheduler) 在源 Observable 和目标观察者之间引入了一个中介观察者,中介负责调度,它使用给定的 scheduler 来调用目标观察者。

实例操作符可能会接收调度器作为参数。

bufferTimedebounceTimedelayauditTimesampleTimethrottleTimetimeIntervaltimeouttimeoutWithwindowTime 这样时间相关的操作符全部接收调度器作为最后的参数,并且默认的操作是在 Rx.Scheduler.async 调度器上。

其他接收调度器作为参数的实例操作符:cachecombineLatestconcatexpandmergepublishReplaystartWith

注意,cachepublishReplay 都接收调度器是因为它们使用了 ReplaySubject 。ReplaySubjects 的构造函数接收一个可选的调度器作为最后的参数,因为 ReplaySubject 可能会处理时间,这只在调度器的上下文中才有意义。默认情况下,ReplaySubject 使用 queue 调度器来提供时钟。

转换成 observables

// 来自一个或多个值
Rx.Observable.of('foo', 'bar');

// 来自数组
Rx.Observable.from([1,2,3]);

// 来自事件
Rx.Observable.fromEvent(document.querySelector('button'), 'click');

// 来自 Promise
Rx.Observable.fromPromise(fetch('/users'));

// 来自回调函数(最后一个参数得是回调函数,比如下面的 cb)
// fs.exists = (path, cb(exists))
var exists = Rx.Observable.bindCallback(fs.exists);
exists('file.txt').subscribe(exists => console.log('Does file exist?', exists));

// 来自回调函数(最后一个参数得是回调函数,比如下面的 cb)
// fs.rename = (pathA, pathB, cb(err, result))
var rename = Rx.Observable.bindNodeCallback(fs.rename);
rename('file.txt', 'else.txt').subscribe(() => console.log('Renamed!'));

创建 observables

在外部产生新事件。

var myObservable = new Rx.Subject();
myObservable.subscribe(value => console.log(value));
myObservable.next('foo');

在内部产生新事件。

var myObservable = Rx.Observable.create(observer => {
  observer.next('foo');
  setTimeout(() => observer.next('bar'), 1000);
});
myObservable.subscribe(value => console.log(value));

选择哪种方式需要根据场景。当你想要包装随时间推移产生值的功能时,普通的 Observable 就已经很好了。使用 Subject,你可以从任何地方触发新事件,并且将已存在的 observables 和它进行连接。

控制流动

// 输入 "hello world"
var input = Rx.Observable.fromEvent(document.querySelector('input'), 'input');

// 过滤掉小于3个字符长度的目标值
input.filter(event => event.target.value.length > 2)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "hel"

// 延迟事件
input.delay(200)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "h" -200ms-> "e" -200ms-> "l" ...

// 每200ms只能通过一个事件
input.throttleTime(200)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "h" -200ms-> "w"

// 停止输入后200ms方能通过最新的那个事件
input.debounceTime(200)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "o" -200ms-> "d"

// 在3次事件后停止事件流
input.take(3)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "hel"

// 直到其他 observable 触发事件才停止事件流
var stopStream = Rx.Observable.fromEvent(document.querySelector('button'), 'click');
input.takeUntil(stopStream)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "hello" (点击才能看到)

产生值

// 输入 "hello world"
var input = Rx.Observable.fromEvent(document.querySelector('input'), 'input');

// 传递一个新的值
input.map(event => event.target.value)
  .subscribe(value => console.log(value)); // "h"

// 通过提取属性传递一个新的值
input.pluck('target', 'value')
  .subscribe(value => console.log(value)); // "h"

// 传递之前的两个值
input.pluck('target', 'value').pairwise()
  .subscribe(value => console.log(value)); // ["h", "he"]

// 只会通过唯一的值
input.pluck('data').distinct()
  .subscribe(value => console.log(value)); // "helo wrd"

// 不会传递重复的值
input.pluck('data').distinctUntilChanged()
  .subscribe(value => console.log(value)); // "helo world"

创建应用

RxJS 是个很好的工具,可以让你的代码更少出错。它是通过使用无状态的纯函数来做到这点的。但是应用是有状态的,那么我们如何将 RxJS 的无状态世界与我们应用的有状态世界连接起来呢?

我们来创建一个只存储值为0的简单状态。每次点击我们想要增加存储在状态中的 count 。

var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
  // 对流进行 scan (reduce) 操作,以获取 count 的值
  .scan(count => count + 1, 0)
  // 每次改变时都在元素上设置 count
  .subscribe(count => document.querySelector('#count').innerHTML = count);

所以产生状态是在 RxJS 的世界中完成的,但最后一行代码中改变 DOM 却是一种副作用。

状态和存储 (State Store)

应用使用状态和存储来保持状态。状态存储在不同的框架中有着不同的名称,像 store、reducer 和 model ,但重点是它们都只是普通的对象。我们还需要处理的是多个 observables 可以更新同一个状态存储。

var increaseButton = document.querySelector('#increase');
var increase = Rx.Observable.fromEvent(increaseButton, 'click')
  // 我们映射到一个函数,它会改变状态
  .map(() => state => Object.assign({}, state, {count: state.count + 1}));

我们在这所做的是将点击事件映射成改变状态的函数。所以我们映射到一个函数,而不是映射到一个值。函数会改变状态存储中的状态。那么现在我们来看下如何实际地做出改变。

var increaseButton = document.querySelector('#increase');
var increase = Rx.Observable.fromEvent(increaseButton, 'click')
  .map(() => state => Object.assign({}, state, {count: state.count + 1}));

// 我们使用初始状态创建了一个对象。每当状态发生变化时,我们会接收到改变状态的函数,
// 并把状态传递给它。然后返回新的状态并准备在下次点击后再次更改状态。
var state = increase.scan((state, changeFn) => changeFn(state), {count: 0});

现在我们还可以再添加几个 observables ,它们同样也可以更改同一个状态存储。

var increaseButton = document.querySelector('#increase');
var increase = Rx.Observable.fromEvent(increaseButton, 'click')
  // 我们再一次映射到一个函数,它会增加 count
  .map(() => state => Object.assign({}, state, {count: state.count + 1}));

var decreaseButton = document.querySelector('#decrease');
var decrease = Rx.Observable.fromEvent(decreaseButton, 'click')
  // 我们还是映射到一个函数,它会减少 count 
  .map(() => state => Object.assign({}, state, {count: state.count - 1}));

var inputElement = document.querySelector('#input');
var input = Rx.Observable.fromEvent(inputElement, 'keypress')
  // 我们还将按键事件映射成一个函数,它会产生一个叫做 inputValue 状态
  .map(event => state => Object.assign({}, state, {inputValue: event.target.value}));

// 我们将这三个改变状态的 observables 进行合并
var state = Rx.Observable.merge(
  increase,
  decrease,
  input
).scan((state, changeFn) => changeFn(state), {
  count: 0,
  inputValue: ''
});

// 我们订阅状态的变化并更新 DOM
state.subscribe((state) => {
  document.querySelector('#count').innerHTML = state.count;
  document.querySelector('#hello').innerHTML = 'Hello ' + state.inputValue;
});

// 为了优化渲染,我们可以检查什么状态是实际上已经发生变化了的
var prevState = {};
state.subscribe((state) => {
  if (state.count !== prevState.count) {
    document.querySelector('#count').innerHTML = state.count;
  }
  if (state.inputValue !== prevState.inputValue) {
    document.querySelector('#hello').innerHTML = 'Hello ' + state.inputValue;
  }
  prevState = state;
});

我们可以采用状态存储的方式,并且结合一些不同的框架和库来进行使用。

Immutable JS

你还可以使用 Immutable JS 来为你的应用创建一个全局的状态存储。Immutable JS 是创建不变的状态存储的好方法,它允许你通过对更改的值进行浅检查来优化渲染。

import Immutable from 'immutable';
import someObservable from './someObservable';
import someOtherObservable from './someOtherObservable';

var initialState = {
  foo: 'bar'
};

var state = Observable.merge(
  someObservable,
  someOtherObservable
).scan((state, changeFn) => changeFn(state), Immutable.fromJS(initialState));

export default state;

现在,你可以在使用的任何 UI 层中导入状态。

import state from './state';

state.subscribe(state => {
  document.querySelector('#text').innerHTML = state.get('foo');
});

React

我们来看一个示例,当组件进入 componentDidMount 生命周期事件时订阅 observable,而当进入 componentWillUnmount 生命周期事件时取消订阅。

import messages from './someObservable';

class MyComponent extends ObservableComponent {
  constructor(props) {
    super(props);
    this.state = {messages: []};
  }
  componentDidMount() {
    this.messages = messages
      // 在数组中累积我们的消息
      .scan((messages, message) => [message].concat(messages), [])
      // 当得到一条新消息时进行渲染
      .subscribe(messages => this.setState({messages: messages}));
  }
  componentWillUnmount() {
    this.messages.unsubscribe();
  }
  render() {
    return (
      
    {this.state.messages.map(message =>
  • {message.text}li>)} </ul> div> ); } } export default MyComponent;

还有许多其他的方式可以使用 React 和 observables。看看这些:

  • rxjs-react-component。它允许你暴露映射到状态更改的 observable 。还可以使用 observable 版的生命周期钩子。

Pipeable 操作符

从5.5版本开始我们提供了 “pipeable 操作符”,它们可以通过 rxjs/operators 来访问 (注意 "operators" 是复数)。相比较于通过在 rxjs/add/operator/* 中以“打补丁”的方式来获取需要用到的操作符,这是一种更好的方式,

注意: 如果使用 rxjs/operators 而不修改构建过程的话会导致更大的包。详见下面的已知问题一节。

重命名的操作符

由于操作符要从 Observable 中独立出来,所以操作符的名称不能和 JavaScript 的关键字冲突。因此一些操作符的 pipeable 版本的名称做出了修改。这些操作符是:

  1. do -> tap
  2. catch -> catchError
  3. switch -> switchAll
  4. finally -> finalize

pipeObservable 的一部分,不需要导入,并且它可以替代现有的 let 操作符。

source$.let(myOperator) -> source$.pipe(myOperator)

参见下面的“构建自己的操作符”。

之前的 toPromise() “操作符”已经被移除了,因为一个操作符应该返回 Observable,而不是 Promise 。现在使用 Observable.toPromise() 的实例方法来替代。

因为 throw 是关键字,你可以在导入时使用 _throw,就像这样: import { _throw } from 'rxjs/observable/throw'

如果前缀_使你困扰的话 (因为一般前缀_表示“内部的 - 不要使用”) ,你也可以这样做:

import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
...
const e = ErrorObservable.create(new Error('My bad'));
const e2 = new ErrorObservable(new Error('My bad too'));

为什么需要 pipeable 操作符?

打补丁的操作符主要是为了链式调用,但它存在如下问题:

  1. 任何导入了补丁操作符的库都会导致该库的所有消费者的 Observable.prototype 增大,这会创建一种依赖上的盲区。如果此库移除了某个操作符的导入,这会在无形之中破坏其他所有人的使用。使用 pipeable 操作符的话,你必须在每个用到它们的页面中都导入你所需要用到的操作符。

  2. 通过打补丁的方式将操作符挂在原型上是无法通过像 rollup 或 webpack 这样的工具进行“摇树优化” ( tree-shakeable ) 。而 pipeable 操作符只是直接从模块中提取的函数而已。

  3. 对于在应用中导入的未使用过的操作符,任何类型的构建工具或 lint 规则都无法可靠地检测出它们。例如,比如你导入了 scan,但后来不再使用了,但它仍会被添加到打包后的文件中。使用 pipeable 操作符的话,如果你不再使用它的简化,lint 规则可以帮你检测到。

  4. 函数组合 ( functional composition )很棒。创建自定义操作符也变得非常简单,它们就像 rxjs 中的其他所有操作符一样。你不再需要扩展 Observable 或重写 lift

什么是 pipeable 操作符?

简而言之,就是可以与当前的 let 操作符一起使用的函数。无论名称起的是否合适,这就是它的由来。基本上来说,pipeable 操作符可以是任何函数,但是它需要返回签名为 (source: Observable) => Observable 的函数。

现在 Observable 中有一个内置的 pipe 方法 (Observable.prototype.pipe),它可以用类似于之前的链式调用的方式来组合操作符 (如下所示)。

There is also a pipe utility function at rxjs/util/pipe that can be used to build reusable pipeable operators from other pipeable operators.

rxjs/util/pipe 中还有一个名为 pipe 的工具函数,它可用于构建基于其他 pipeable 操作符的可复用的 pipeable 操作符。

用法

你只需在 'rxjs/operators' (注意是复数!) 中便能提取出所需要的任何操作符。还推荐直接导入所需的 Observable 创建操作符,如下面的 range 所示:

import { range } from 'rxjs/observable/range';
import { map, filter, scan } from 'rxjs/operators';

const source$ = range(0, 10);

source$.pipe(
  filter(x => x % 2 === 0),
  map(x => x + x),
  scan((acc, x) => acc + x, 0)
)
.subscribe(x => console.log(x))

轻松创建自定义操作符

实际上,你可以一直let 来完成...,但是现在创建自定义操作符就像写个函数一样简单。注意,你可以将你的自定义操作符和其他的 rxjs 操作符无缝地组合起来。

import { interval } from 'rxjs/observable/interval';
import { filter, map, take, toArray } from 'rxjs/operators';

/**
 * 取每第N个值的操作符
 */
const takeEveryNth = (n: number) => <T>(source: Observable<T>) =>
  new Observable<T>(observer => {
    let count = 0;
    return source.subscribe({
      next(x) {
        if (count++ % n === 0) observer.next(x);
      },
      error(err) { observer.error(err); },
      complete() { observer.complete(); }
    })
  });

/**
 * 还可以使用现有的操作符
 */
const takeEveryNthSimple = (n: number) => <T>(source: Observable<T>) =>
  source.pipe(filter((value, index) => index % n === 0 ))

/**
 * 因为 pipeable 操作符返回的是函数,还可以进一步简化
 */
const takeEveryNthSimplest = (n: number) => filter((value, index) => index % n === 0);

interval(1000).pipe(
  takeEveryNth(2),
  map(x => x + x),
  takeEveryNthSimple(3),
  map(x => x * x),
  takeEveryNthSimplest(4),
  take(3),
  toArray()
)
.subscribe(x => console.log(x));
// [0, 2304, 9216]

已知问题

TypeScript < 2.4

在2.3及以下版本的 TypeScript 中,需要在传递给操作符的函数中添加类型,因为 TypeScript 2.4之前的版本无法推断类型。在TypeScript 2.4中,类型可以通过组合来正确地推断出来。

TS 2.3及以下版本

range(0, 10).pipe(
  map((n: number) => n + '!'),
  map((s: string) => 'Hello, ' + s),
).subscribe(x => console.log(x))

TS 2.4及以上版本

range(0, 10).pipe(
  map(n => n + '!'),
  map(s => 'Hello, ' + s),
).subscribe(x => console.log(x))

构建和摇树优化

当从清单文件导入(或重新导出)时,应用的打包文件有时会增大。现在可以从 rxjs/operators 导入 pipeable 操作符,但如果不更新构建过程的话,会经常导致应用的打包文件更大。这是因为默认情况下 rxjs/operators 会解析成 rxjs 的 CommonJS 输出。

为了使用新的 pipeable 操作符而不增加打包尺寸,你需要更新 Webpack 配置。这只适用于 Webpack 3+ ,因为需要依赖 Webpack 3中的新插件 ModuleConcatenationPlugin

路径映射

伴随 rxjs 5.5版本一同发布的是使用ES5 和 ES2015 两种语言级别的 ECMAScript 模块格式 (导入和导出)。你可以在 node_modules/rxjs/_esm5node_modules/rxjs/_esm2015 下面分别找到这两个分发版本 ("esm"表示 ECMAScript 模块,数字"5"或"2015"代表 ES 语言级别)。在你的应用源码中,你应该从 rxjs/operators 导入,但在 Webpack 配置文件中,你需要将导入重新映射为 ESM5 (或 ESM2015) 版本。

如果 require('rxjs/_esm5/path-mapping'),你将接收一个函数,该函数返回一个键值对的对象,该对象包含每个输入映射到磁盘上的文件位置。像下面这样使用该映射:

webpack.config.js

简单配置:

const rxPaths = require('rxjs/_esm5/path-mapping');
const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: 'index.js',
  output: 'bundle.js',
  resolve: {
    // 使用 "alias" 键来解析成 ESM 分发版
    alias: rxPaths()
  },
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

更多完整配置 (接近真正场景):

const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DashboardPlugin = require('webpack-dashboard/plugin');
const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = nodeEnv === 'production';
const rxPaths = require('rxjs/_esm5/path-mapping');

var config = {
    devtool: isProd ? 'hidden-source-map' : 'cheap-eval-source-map',
    context: path.resolve('./src'),
    entry: {
        app: './index.ts',
        vendor: './vendor.ts'
    },
    output: {
        path: path.resolve('./dist'),
        filename: '[name].bundle.js',
        sourceMapFilename: '[name].map',
        devtoolModuleFilenameTemplate: function (info) {
            return "file:///" + info.absoluteResourcePath;
        }
    },
    module: {
        rules: [
            { enforce: 'pre', test: /\.ts$|\.tsx$/, exclude: ["node_modules"], loader: 'ts-loader' },
            { test: /\.html$/, loader: "html" },
            { test: /\.css$/, loaders: ['style', 'css'] }
        ]
    },
    resolve: {
        extensions: [".ts", ".js"],
        modules: [path.resolve('./src'), 'node_modules'],
        alias: rxPaths()
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env': { // eslint-disable-line quote-props
                NODE_ENV: JSON.stringify(nodeEnv)
            }
        }),
        new webpack.HashedModuleIdsPlugin(),
        new webpack.optimize.ModuleConcatenationPlugin(),
        new HtmlWebpackPlugin({
            title: 'Typescript Webpack Starter',
            template: '!!ejs-loader!src/index.html'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: Infinity,
            filename: 'vendor.bundle.js'
        }),
        new webpack.optimize.UglifyJsPlugin({
            mangle: false,
            compress: { warnings: false, pure_getters: true, passes: 3, screw_ie8: true, sequences: false },
            output: { comments: false, beautify: true },
            sourceMap: false
        }),
        new DashboardPlugin(),
        new webpack.LoaderOptionsPlugin({
            options: {
                tslint: {
                    emitErrors: true,
                    failOnHint: true
                }
            }
        })
    ]
};

module.exports = config;

无法控制构建过程

如果你无法控制构建过程(或者无法更新至 Webpack 3+)的话,上述解决方案将不适合你。所以,从 rxjs/operators 导入很可能让应用的打包文件尺寸更大。但还是有解决办法的,你需要使用更深一层的导入,有点类似于5.5版本之前导入 pipeable 操作符的方式。

将:

import { map, filter, reduce } from 'rxjs/operators';

变成:

import { map } from 'rxjs/operators/map';
import { filter } from 'rxjs/operators/filter';
import { reduce } from 'rxjs/operators/reduce';

编写弹珠测试

“弹珠测试”是使用一种叫做 TestScheduler 的专用的虚拟调度器 (VirtualScheduler) 的测试。它们可以使我们以同步且可靠的方式来测试异步操作。“弹珠符号”是源自许多人的教程与文档,例如,@jhusain、@headinthebox、@mattpodwysocki 和 @staltz 。实际上,是由 @staltz 首先提出建议将其作为创建单元测试的 DSL (Domain Specific Language 领域专用语言),并且它已经经过改造并被采纳。

链接

  • 贡献指南
  • 行为准则

基础方法

单元测试添加了一些辅助方法,以使创建测试更容易。

  • hot(marbles: string, values?: object, error?: any) - 创建一个“热的” Observable (Subject),它将在测试开始时表现得 好像已经在“运行中”了。一个有趣的不同点是 hot 弹珠允许使用^字符来标志“零帧”所在处。这正是 Observables 订阅的起始点, 同时也是测试的起始点。
  • cold(marbles: string, values?: object, error?: any) - 创建一个“冷的” Observable ,当测试开始时它便开始订阅。
  • expectObservable(actual: Observable).toBe(marbles: string, values?: object, error?: any) - 当 TestScheduler flush时, 安排一个断言。在 jasmine 的 it 块结束处 TestScheduler 会自动进行 flush 。
  • expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string) - 类似 expectObservable ,当 TestScheduler flush时,安排一个断言。cold()hot() 都返回带有subscriptions 属性 (类型为 SubscriptionLog[])的 Observable 。将 subscriptions 作为参数传给 expectSubscriptions 以断言是否匹配在 toBe() 中 给定的 expectObservable 弹珠图。Subscription 的弹珠图与 Observable 的弹珠图略有不同。详情请参见下面。

hotcold 的默认行为是符合人类认知的

hotcold 方法中,弹珠图中指定的值的字符都会作为字符串发出,除非将 values 参数传给了方法。因此:

hot('--a--b') 会发出 "a""b" ,但

hot('--a--b', { a: 1, b: 2 }) 会发出 12

同样的,未指明的错误就只发出默认字符串 "error",所以:

hot('---#') 会发出错误 "error" , 但

hot('---#', null, new SpecialError('test')) 会发出 new SpecialError('test')

弹珠语法

弹珠语法是用字符串表示随“时间”流逝而发生的事件。任何弹珠字符串的首字符永远都表示“零帧”。“帧”是有点类似于虚拟毫秒的概念。

  • "-" 时间: 10“帧”的时间段。
  • "|" 完成: 表示 Observalbe 成功完成。这是 Observable 生产者所发出的 complete() 信号。
  • "#" 错误: 终止 Observable 的错误。 这是 Observable 生产者所发出的 error() 信号。
  • "a" 任意字符: 所有其他字符表示由 Observalbe 生产者所发出的 next() 信号的值。
  • "()" 同步分组: 当多个事件需要在同一帧中同步地发出,用圆括号来将这些事件聚集在一起。你可以以这种形式来聚合值、完成或错误。 起始 ( 的位置决定了值发出的时间。
  • "^" 订阅时间点: (只适用于热的 Observabe) 显示测试 Observable 订阅热的 Observable 的点。它是此 Observable 的“零帧”,在 ^ 前的所有帧都将是无效的。

示例

'-''------': 相当于 Observable.never(),或一个从不发出值或完成的 Observable

|: 相当于 Observable.empty()

#: 相当于 Observable.throw()

'--a--': 此 Observable 等待20“帧”后发出值 a ,然后永远不发出 complete

'--a--b--|: 在20帧处发出 a,在50帧处发出 b,然后在80帧处发出 complete

'--a--b--#: 在20帧处发出 a,在50帧处发出 b,然后在80帧处发出 error

'-a-^-b--|: 这是个热的 Observable ,在-20帧处发出 a,然后在20帧处发出 b ,在50帧出发出 complete

'--(abc)-|': 在20帧处发出 abc,然后在80帧处发出 complete

'-----(a|)': 在50帧处发出 acomplete

Subscription 的弹珠语法

Subscription 的弹珠语法与常见的弹珠语法略有不同。它表示随“时间”流逝而发生的订阅取消订阅的时间点。在此类图中不应该出现其他类型的事件。

  • "-" 时间: 10“帧”的时间段。
  • "^" 订阅时间点: 显示订阅发生的时间点。
  • "!" 取消订阅时间点: 显示取消订阅发生的时间点。

在 Subscription 的弹珠语法中 ^! 时间点最多只有一个。 除此之外,- 字符是唯一允许出现的字符。

示例

'-''------': 没有订阅发生。

'--^--': 在20帧处发生了订阅,并且订阅没有被取消。

'--^--!-: 在20帧处发生了订阅,在50帧处订阅被取消了

测试剖析

一个基础的测试看起来应该是下面这样的:


var e1 = hot('----a--^--b-------c--|');
var e2 = hot(  '---d-^--e---------f-----|');
var expected =      '---(be)----c-f-----|';

expectObservable(e1.merge(e2)).toBe(expected);
  • hot observables 的 ^ 字符应该永远是对齐的。
  • cold observables 或预期 observables 的首字符hot observables 的 ^ 应该永远彼此对齐的。
  • 尽量使用默认的发送值。当必要的时候才指定值 。

使用指定值的测试用例:

var values = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  x: 1 + 3, // a + c
  y: 2 + 4, // b + d
}
var e1 =    hot('---a---b---|', values);
var e2 =    hot('-----c---d---|', values);
var expected =  '-----x---y---|';

expectObservable(e1.zip(e2, function(x, y) { return x + y; }))
  .toBe(expected, values);
  • 使用同一个散列表来查找所有的值,这可以确保多次使用的同一个字符有着同样的值。
  • 将结果值所表示的含义表达的越明显越好,毕竟这些是测试,我们最想要的是代码的清晰程度,而不是执行效率,所以 x: 1 + 3, // a + cx: 4 要好。前者传达了为什么是4,而后者并没有做到这点。

使用 subscription 断言的测试用例:

var x = cold(        '--a---b---c--|');
var xsubs =    '------^-------!';
var y = cold(                '---d--e---f---|');
var ysubs =    '--------------^-------------!';
var e1 = hot(  '------x-------y------|', { x: x, y: y });
var expected = '--------a---b----d--e---f---|';

expectObservable(e1.switch()).toBe(expected);
expectSubscriptions(x.subscriptions).toBe(xsubs);
expectSubscriptions(y.subscriptions).toBe(ysubs);
  • xsubs 图和 ysubs 图的开头与 expected 图对齐。
  • 注意冷的 observable x 取消订阅的同时 e1 发出了 y

在大多数测试中,是没有必要测试订阅时间点和取消订阅时间点的,它们要不就非常明显,要不就在 expected 图中有所暗示。在这些情况下是不需要编写 subscription 断言的。在有内部 subscriptions 或冷的 observables 有多个订阅者的测试用例中,这些 subscription 断言还是有用的。

基于测试生成 PNG 弹珠图

通常,Jasmine 中的测试用例都是这样写的:it('should do something', function () { /* ... */ }) 。要想时测试用例可以用来生成 PNG 弹珠图,你必须使用 asDiagram(label) 函数,像这样:

it.asDiagram(operatorLabel)('should do something', function () {
  // ...
});

举例来说,对于 zip 操作符,我们可以这样写:

it.asDiagram('zip')('should zip by concatenating', function () {
  var e1 =    hot('---a---b---|');
  var e2 =    hot('-----c---d---|');
  var expected =  '-----x---y---|';
  var values = { x: 'ac', y: 'bd' };

  var result = e1.zip(e2, function(x, y) { return String(x) + String(y); });

  expectObservable(result).toBe(expected, values);
});

然后当运行 npm run tests2png 时,这个测试用例会解析并且在 img/ 文件夹下创建一个 PNG 文件 zip.png (文件名取决于 ${operatorLabel}.png)。

创建操作符

创建 RxJS 的操作符可以有多种方式。在这个版本的 RxJS 中,性能是首要考虑因素,因此,在此库中将创建的操作符依附到现有结构中的这种方式可能不那么直截了当。这是一个尝试性的文档,以告诉你如何创建一个操作符,这个操作符可以是你自己使用,也可以是用来添加到库中。

想了解如何为本库开发一个自定义操作符,请参见下面。

自动动手来为终端用户提供自定义的操作符

指南

在大多数情况下,用户可能想要创建一个只在它们的应用中使用的操作符。可以用任何开发者觉得适合的方式来进行开发,但这里有一些指导方针:

  1. 操作符应该永远返回一个 Observable 。你正在对一个未知的集合执行操作以创建一个新的集合。只有返回一个新的集合才有意义。如果你创建 了一个返回非 Observable 的方法,那么它就不是一个操作符,好吧。
  2. 确保对你的操作符返回的 Observalbe 内部所创建的 subscriptions 进行管理。你的操作符需要订阅返回 Observable 中的源(或 this), 确保它是作为取消订阅处理方法或 subscription 的一部分返回的。
  3. 确保处理传入函数中的异常。如果你实现的操作符接收函数作为参数,当你调用它时,你会想要将其包裹在 try/catch 中并发送 错误到 observable 的 error() 路径。
  4. 确保在返回的 Observable 的取消订阅处理方法中释放稀缺资源。如果你设置了事件处理方法,或 web socket,或一些其他类似的,取消订阅 方法是移除事件处理方法和关闭 socket 的好地方。

示例

function mySimpleOperator(someCallback) {
   // 我们可以在这写 `var self = this;` 以保存 `this` ,但看下一条注释
   return Observable.create(subscriber => {
     // 因为我们正在箭头函数中,`this` 来自于外部作用域。
     var source = this;

     // 保存我们的内部 subscription
     var subscription = source.subscribe(value => {
       // 重点:从用户提供的回调函数中捕获错误
       try {
         subscriber.next(someCallback(value));
       } catch(err) {
         subscriber.error(err);
       }
     },
     // 确保处理错误,然后视情况而定进行完成并发送它们
     err => subscriber.error(err),
     () => subscriber.complete());

     // 现在返回
     return subscription;
   });
}

将操作符添加到 Observalbe 中

有几种方法可以做到这点。至于使用哪种方法取决于需求和偏好:

1) 使用 ES7 函数绑定操作符 (::),在像 BabelJS 这样的编译器中是可用的:

someObservable::mySimpleOperator(x => x + '!');

2) 创建你自己的 Observable 子类并重载 lift 方法将其返回:

class MyObservable extends Observable {
  lift(operator) {
    const observable = new MyObservable(); //<-- 这里是关键点
    observable.source = this;
    observable.operator = operator;
    return observable;
  }

  // 放在这里 .. 或 ..
  customOperator() {
    /* 做些事情并返回 Observable  */
  }
}

// ... 放在这里...
MyObservable.prototype.mySimpleOperator = mySimpleOperator;

3) 直接在 Observable.prototype 上打补丁:

Observable.prototype.mySimpleOperator = mySimpleOperator;

// ... 然后 .../

someObservable.mySimpleOperator(x => x + '!');

作为纯函数的操作符

如果你不想在 Observable 原型上打补丁的话,还可以编写纯函数作为操作符,此函数接收输入 Observable 作为参数来替代对 this 关键字的依赖。

示例实现:

function mySimpleOperator(someCallback) {
  // 注意这里返回的是函数
  return function mySimpleOperatorImplementation(source) {
    return Observable.create(subscriber => {
      var subscription = source.subscribe(value => {
        try {
          subscriber.next(someCallback(value));
        } catch(err) {
          subscriber.error(err);
        }
      },
      err => subscriber.error(err),
      () => subscriber.complete());

      return subscription;
   });
  }
}

现在可以使用 Observable 上的 pipe() 方法:

const obs = someObservable.pipe(mySimpleOperator(x => x + '!'));

将操作符作为独立的库发布

我们强烈推荐你将使用了 let 的纯函数的自定义操作符作为独立的 npm 包。RxJS 核心已经超过100个操作符,我们不应该再增加额外的操作符了,除非它们是绝对必要的,并且提供了现有操作符无法提供的功能。

将其作为一个独立的库发布将保证你的操作符能够立即为社区所用,并且这是对 RxJS 社区生态的一种促进与发展,而不是让 RxJS 库变得愈发笨重。但是,在某些情况下,新操作符还是应该添加到核心库中。

创建加入到此库中的操作符

在提议将操作符加入 RxJS 库之前,请先将其作为独立的库进行发布。 参见上一节。

要创建加入到此库中的操作符,最好是基于现有的成果来工作。像 filter 这样的操作符会是不错的开始。没有人会期望你在读完本章后会立即成为一个专家级的操作符贡献者。

如果你觉得自己很困惑,请不要担心。按照仓库中之前的示例,提交 PR,我们同你并肩作战。

当你为本库开发操作符时,希望这里所提供的信息能帮你做出决策。当开发操作符时,这里有一些需要知道并(尝试)理解的知识:

  1. 实际上所有的操作符方法都是在 Observable 之外的单独模块中创建的。这是因为开发者可以通过将操作符方法提取出来并在自己的模块中 将其方法添加到 observable 中来“构建自己的 observable ”。这也就意味着可以操作符可以单独加入并直接使用,无论是在 Babel 中使用 ES7 函数绑定操作符还是通过 .call() 来使用。
  2. 每个操作符都有一个 Operator 类。Operator 类实际上是一个 Subscriber “工厂”。它会被传递给 lift 方法来使“魔法”发生。 它唯一的工作就是在 subscription 上创建操作符的 Subscriber 实例。
  3. 每个操作符都有一个 Subscriber 类。 这个类完成了操作符的所有逻辑。它的工作就是处理接下来的值(通常是通过重载 _next() ), 然后将其转发给 destination,即链中的下一个观察者。
    • 重要的是要注意,在任何 Subscriber 服务上设置的 destination 观察者不仅仅是传递的事件的目的地,如果 destinationSubscriber 的话,它也用于设置共享底层的 Subscription ,其实际上也是 Subscriber,并且是链中的第一个 Subscriber
    • Subscribers 都有 addremove 方法,用来添加和移除共享底层的 subscription 的内部 subscriptions 。
    • 当你订阅 Observable 时,传递的函数或观察者用于创建链的最终 destination Subscriber 。它是 Subscriber ,实际上也是操作符链的共享 Subscription

为操作符提交 PR

请为每个要添加到 RxJS 的新操作符完成以下步骤,并作为一个 Pull Request:

  • 将操作符添加到 Rx 中
  • 必须有一个 -spec.ts 的测试文件来涵盖典型用例,请使用弹珠图测试
  • 如果可以的话,请再编写一个 asDiagram 测试用例,用于生成 PNG 弹珠图
  • spec 文件应在末尾处有类型定义测试,以验证各种用例的类型定义
  • 操作符必须在实现文件中用 JSDoc 风格的文档,同样包括 PNG 弹珠图
  • 操作符应该在文件 doc/operators.md 中的操作符分类中列举出来
  • 还应该在操作符决策树文件 doc/decision-tree-widget/tree.yml 中将其加入
  • 如果操作符与 RxJS v4 中相对应的有所不同的话,你可能需要更新文件 MIGRATION.md

内部 Subscriptions

“内部 subscriber” 或“内部 subscription” 是在操作符的主要订阅者中创建的任意 subscription 。例如,如果你要创建一个自己的 “merge” 操作符,在你想要 “合并” 的源 Observable 上接收到的 Observable 需要被订阅。这些 subscriptions 就是内部 subscriptions 。在本库中关于内部 subscriptions 的一个有趣点就是,如果你传递给并设置了 destination ,它们会尝试为它们的 unsubscribe 调用使用 destination 。意味着如果你调用它们的 unsubscribe,它可能不会做任何事情。因此,通常不会为内部 subscriptions 设置 destination 。一个例子可能就是 switch 操作符,它具有单独的底层内部 subscription ,此内部 subscription 取消订阅需要不依赖于主 subscription 。

如果你发现自己创建了内部 subscriptions ,你可能真的需要检查,看看 _isScalar 是否传给了 observable ,因为如果是的话,你可以直接将 value 提取出来,并当操作标量 observable 时提升操作符性能。作为参考,标量 observable 是具有单一静态值的 observable 。Observable.of('foo') 会返回一个 ScalarObservable,同样的,resolve 过的 `PromiseObservable 也充当标量。

文档地址

https://cn.rx.js.org/identifiers.html

github链接

https://github.com/ReactiveX/rxjs

js